From 8d0f209a8ade0f8201aab54fbfa4261f0cb1ec41 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Mon, 12 Oct 2026 15:32:16 -0700 Subject: [PATCH 001/428] fix(libc): convert scanf integers as FreeBSD does on Windows --- .../libs/prx/libc/include/WindowsScanning.hpp | 26 +++++++++++++++-- core/libs/tests/Formatting.cpp | 29 ++++++++++++++++++- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsScanning.hpp b/core/libs/prx/libc/include/WindowsScanning.hpp index 77bbf1541e..aa71506809 100644 --- a/core/libs/prx/libc/include/WindowsScanning.hpp +++ b/core/libs/prx/libc/include/WindowsScanning.hpp @@ -16,6 +16,14 @@ inline int ScanWindowsArguments_nid_no_patch(const char* format, const void* sou std::vector pointers; std::vector suppressedEnds(static_cast(std::count(format, format + std::strlen(format), '%')), -1); size_t suppressedCount = 0; + struct NarrowInteger { + void* destination; + size_t size; + long long value; + int end; + }; + std::vector narrowIntegers; + narrowIntegers.reserve(suppressedEnds.size()); FormatArguments args(source); while (*format) { const char value = *format++; @@ -26,7 +34,7 @@ inline int ScanWindowsArguments_nid_no_patch(const char* format, const void* sou if (suppressed) translated += *format++; while (*format >= '0' && *format <= '9') translated += *format++; std::string length; - if (*format && std::strchr("hljztL", *format)) { + if (*format && std::strchr("hljztLq", *format)) { length += *format++; if ((length == "h" && *format == 'h') || (length == "l" && *format == 'l')) length += *format++; } @@ -37,7 +45,8 @@ inline int ScanWindowsArguments_nid_no_patch(const char* format, const void* sou return EOF; } ++format; - if (std::strchr("diouxXn", conversion) && (length == "l" || length == "j" || length == "z" || length == "t")) + const bool narrow = !suppressed && std::strchr("diouxX", conversion) && (length.empty() || length == "h" || length == "hh"); + if (narrow || (std::strchr("diouxXn", conversion) && (length == "l" || length == "j" || length == "z" || length == "t" || length == "q"))) translated += "ll"; else translated += length; translated += conversion; @@ -48,13 +57,24 @@ inline int ScanWindowsArguments_nid_no_patch(const char* format, const void* sou if (*format != ']') { errno = 22; return EOF; } translated += *format++; } - if (!suppressed) pointers.push_back(args.Next()); + if (narrow) { + narrowIntegers.push_back({args.Next(), length.empty() ? sizeof(int) : length == "h" ? sizeof(short) : sizeof(char), 0, -1}); + translated += "%n"; + pointers.push_back(&narrowIntegers.back().value); + pointers.push_back(&narrowIntegers.back().end); + } else if (!suppressed) pointers.push_back(args.Next()); else if (conversion != 'n') { translated += "%n"; pointers.push_back(&suppressedEnds[suppressedCount++]); } } const int result = scan(translated.c_str(), reinterpret_cast(pointers.data())); + for (const auto& integer : narrowIntegers) { + if (integer.end < 0) continue; + if (integer.size == sizeof(char)) *static_cast(integer.destination) = static_cast(integer.value); + else if (integer.size == sizeof(short)) *static_cast(integer.destination) = static_cast(integer.value); + else *static_cast(integer.destination) = static_cast(integer.value); + } if (result == EOF && std::any_of(suppressedEnds.begin(), suppressedEnds.begin() + suppressedCount, [](int end) { return end >= 0; })) return 0; return result; diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index 1b2fdf7b3b..681655761c 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -286,4 +286,31 @@ static void CheckAllocatingFormat() { free_nid_postfix(allocated); } -int main() { RunChecks(); CheckAllocatingFormat(); return CheckWidePrecision() ? 0 : 1; } +static void CheckIntegerScanWidths() { + int value = 7; + *__error_nid_postfix() = 0; + Require(sscanf_nid_postfix("2147483648", "%d", &value) == 1 && value == -2147483647 - 1 && *__error_nid_postfix() == 0); + Require(sscanf_nid_postfix("0xFFFFFFFF", "%i", &value) == 1 && value == -1 && *__error_nid_postfix() == 0); + Require(sscanf_nid_postfix("99999999999999999999", "%d", &value) == 1 && value == -1 && *__error_nid_postfix() == 34); + unsigned unsignedValue = 7; + Require(sscanf_nid_postfix("4294967296", "%u", &unsignedValue) == 1 && unsignedValue == 0); + Require(sscanf_nid_postfix("100000001", "%x", &unsignedValue) == 1 && unsignedValue == 1); + Require(sscanf_nid_postfix("40000000001", "%o", &unsignedValue) == 1 && unsignedValue == 1); + short shortValue = 7; + Require(sscanf_nid_postfix("5000000000", "%hd", &shortValue) == 1 && shortValue == -3584); + signed char charValue = 7; + Require(sscanf_nid_postfix("300", "%hhd", &charValue) == 1 && charValue == 44); + long long quad = 7; + Require(sscanf_nid_postfix("3000000000", "%qd", &quad) == 1 && quad == 3000000000ll); + unsigned long long unsignedQuad = 7; + Require(sscanf_nid_postfix("ffffffffff", "%qx", &unsignedQuad) == 1 && unsignedQuad == 0xffffffffffull); + int first = 7; + int second = 7; + int position = 0; + Require(sscanf_nid_postfix("12 34", "%d %d%n", &first, &second, &position) == 2 && first == 12 && second == 34 && position == 5); + value = 7; + Require(sscanf_nid_postfix("x", "%d", &value) == 0 && value == 7); + Require(sscanf_nid_postfix("1 x", "%d %d", &first, &value) == 1 && first == 1 && value == 7); +} + +int main() { RunChecks(); CheckAllocatingFormat(); CheckIntegerScanWidths(); return CheckWidePrecision() ? 0 : 1; } From 6bdbad4cab31ed4d9f4cef70327eb4b838500441 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:16:23 -0700 Subject: [PATCH 002/428] fix(libSceNet): follow FreeBSD inet text rules sceNetInetPton and sceNetInetNtop called Winsock's InetPtonA and InetNtopA on Windows. Those print IPv4-translated and ISATAP addresses with a dotted tail, print ::ffff:0.0.0.1 as ::ffff:0:1, and accept a leading zero in a dotted tail, where FreeBSD does neither. Both now convert with libkernel's inet_pton and inet_ntop, which follow the FreeBSD 11.4 rules since #3530. --- core/libs/prx/libSceNet/Export.cpp | 29 ++++++----------------------- core/libs/tests/GuestSceNet.cpp | 12 ++++++++++++ docs/dev/debt/libSceNet.md | 1 + 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 1ea3b505ab..c267be65e6 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -46,7 +46,6 @@ constexpr int NET_EBADF = 9; constexpr int NET_EACCES = 13; constexpr int NET_EFAULT = 14; constexpr int NET_EINVAL = 22; -constexpr int NET_ENOSPC = 28; constexpr int NET_EDOM = 33; constexpr int NET_EAGAIN = 35; constexpr int NET_EINPROGRESS = 36; @@ -326,6 +325,8 @@ int* errno_slot() { } extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI __inet_pton_nid_postfix(int family, const char* text, void* destination); +extern "C" const char* APS5_VABI __inet_ntop_nid_postfix(int family, const void* source, char* destination, std::uint32_t capacity); void set_errno(int err) { *errno_slot() = err; @@ -1211,11 +1212,7 @@ int APS5_VABI sceNetInetPton(int af, const char* src, void* dst) { if (af != NET_AF_INET && af != NET_AF_INET6) { return fail(NET_EAFNOSUPPORT); } -#ifdef _WIN32 - return InetPtonA(af == NET_AF_INET ? AF_INET : AF_INET6, src, dst); -#else - return ::inet_pton(af == NET_AF_INET ? AF_INET : AF_INET6, src, dst); -#endif + return __inet_pton_nid_postfix(af, src, dst); } const char* APS5_VABI sceNetInetNtop(int af, const void* src, char* dst, uint32_t size) { @@ -1227,23 +1224,9 @@ const char* APS5_VABI sceNetInetNtop(int af, const void* src, char* dst, uint32_ set_errno(NET_EAFNOSUPPORT); return nullptr; } - char text[INET6_ADDRSTRLEN]; -#ifdef _WIN32 - const auto* result = InetNtopA(af == NET_AF_INET ? AF_INET : AF_INET6, const_cast(src), text, sizeof(text)); -#else - const auto* result = ::inet_ntop(af == NET_AF_INET ? AF_INET : AF_INET6, src, text, sizeof(text)); -#endif - if (!result) { - set_errno(native_error()); - return nullptr; - } - const auto length = std::strlen(text) + 1; - if (length > size) { - set_errno(NET_ENOSPC); - return nullptr; - } - std::memcpy(dst, text, length); - return dst; + const char* result = __inet_ntop_nid_postfix(af, src, dst, size); + if (!result) set_errno(*__error_nid_postfix()); + return result; } int APS5_VABI sceNetEtherNtostr(const NetEtherAddr* n, char* str, size_t len) { diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 2ad9a8a605..901c052dcf 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -379,6 +379,13 @@ static bool HostIpv6LoopbackUnavailable() { #endif } +static void CheckRejectedAddressText(const char* text) { + std::array address; + address.fill(0xa5); + Require(sceNetInetPton(28, text, address.data()) == 0); + Require(address[0] == 0xa5 && address[15] == 0xa5); +} + static void CheckLinkLocalAllNodesIpv6() { Require(reinterpret_cast(sce_net_in6addr_linklocal_allnodes) % alignof(std::uint32_t) == 0); std::array address{}; @@ -479,6 +486,11 @@ int main() { CheckAddressText(2, "127.0.0.1"); CheckAddressText(2, "255.255.255.255"); CheckAddressText(28, "::1"); + CheckAddressText(28, "::ffff:0.0.0.1"); + CheckAddressText(28, "::ffff:0:102:304"); + CheckAddressText(28, "fe80::5efe:102:304"); + CheckRejectedAddressText("::01.2.3.4"); + CheckRejectedAddressText("::ffff:01.2.3.4"); CheckAddressText(28, "1234:5678:9abc:def0:1234:5678:9abc:def0"); CheckLinkLocalAllNodesIpv6(); CheckUnspecifiedIpv6(); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index c70d4323dc..733b160c3d 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -5,6 +5,7 @@ - PPSA01530 and PPSA13356 imports declared without parameters, signatures unknown, added without an implementation because the titles fail to load without them: [sceNetShowIfconfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet), [sce::Json::Value::setNullAccessCallBack](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), and [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad), whose real name is unknown. [_sceUltWaitingQueueResourcePoolCreate, _sceUltSemaphoreCreate, _sceUltMutexOptParamInitialize, _sceUltMutexCreate, _sceUltQueueDataResourcePoolCreate and _sceUltQueueCreate](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) forward to the exports without the underscore; that their argument lists match is inferred from the other underscore Ult exports, not measured. - [sceHttp2GetMemoryPoolStats, sceSslGetMemoryPoolStats, sceNetGetMemoryPoolStats](../../../core/libs/prx/libSceNet/Export.cpp) (libSceHttp2, libSceSsl, libSceNet) - the signatures `(int id, stats*)`, the layouts (pool size, peak and current use, and for Http2 and Ssl a 32-bit reserved field) and the error codes were measured on a PS5 (firmware 13.60). The pool size is the one given at init or pool creation; nothing is allocated from these pools, so the peak and current use are 0, where the console already reports use right after initialization (0x15C0 bytes for Ssl, 0x1640 for Http2 and 4 for a Net pool) - [sceNetShowNetstat](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - imported by PPSA11386 and not called during its start-up; unknown signature, declared without parameters +- [sceNetInetPton, sceNetInetNtop](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - convert the text with libkernel's [inet_pton and inet_ntop](../../../core/libs/prx/libkernel/Socket/src/AddressConversion.cpp), so they take the same FreeBSD 11.4 text forms, which are unverified on a console. The checks and errors before the conversion are libSceNet's own and unchanged ### Functional From a7f3f9cff1eac16ec1ef4fbba70eb186784b2c5d Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 13 Oct 2026 03:40:45 -0700 Subject: [PATCH 003/428] test(libc): run CheckIntegerScanWidths from RunChecks #5070 adds its own check before main() and rewrites the one-line main(), so defining CheckIntegerScanWidths there and calling it from main() made the two pull requests conflict. It now sits before RunChecks and runs after the other scanf checks. --- core/libs/tests/Formatting.cpp | 57 +++++++++++++++++----------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index 681655761c..c6712ec5a4 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -122,6 +122,33 @@ static bool CheckWidePrecision() { return correct; } +static void CheckIntegerScanWidths() { + int value = 7; + *__error_nid_postfix() = 0; + Require(sscanf_nid_postfix("2147483648", "%d", &value) == 1 && value == -2147483647 - 1 && *__error_nid_postfix() == 0); + Require(sscanf_nid_postfix("0xFFFFFFFF", "%i", &value) == 1 && value == -1 && *__error_nid_postfix() == 0); + Require(sscanf_nid_postfix("99999999999999999999", "%d", &value) == 1 && value == -1 && *__error_nid_postfix() == 34); + unsigned unsignedValue = 7; + Require(sscanf_nid_postfix("4294967296", "%u", &unsignedValue) == 1 && unsignedValue == 0); + Require(sscanf_nid_postfix("100000001", "%x", &unsignedValue) == 1 && unsignedValue == 1); + Require(sscanf_nid_postfix("40000000001", "%o", &unsignedValue) == 1 && unsignedValue == 1); + short shortValue = 7; + Require(sscanf_nid_postfix("5000000000", "%hd", &shortValue) == 1 && shortValue == -3584); + signed char charValue = 7; + Require(sscanf_nid_postfix("300", "%hhd", &charValue) == 1 && charValue == 44); + long long quad = 7; + Require(sscanf_nid_postfix("3000000000", "%qd", &quad) == 1 && quad == 3000000000ll); + unsigned long long unsignedQuad = 7; + Require(sscanf_nid_postfix("ffffffffff", "%qx", &unsignedQuad) == 1 && unsignedQuad == 0xffffffffffull); + int first = 7; + int second = 7; + int position = 0; + Require(sscanf_nid_postfix("12 34", "%d %d%n", &first, &second, &position) == 2 && first == 12 && second == 34 && position == 5); + value = 7; + Require(sscanf_nid_postfix("x", "%d", &value) == 0 && value == 7); + Require(sscanf_nid_postfix("1 x", "%d %d", &first, &value) == 1 && first == 1 && value == 7); +} + __attribute__((noinline)) static void APS5_VABI RunChecks() { char buffer[1024]; Require(FormatList(buffer, sizeof(buffer), "Mount requested: %d", 0) == 18); @@ -270,6 +297,7 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { Require(ScanListS("123 test", "%d %s", &scanNum, scanWord, 8u) == 2 && scanNum == 123 && std::strcmp(scanWord, "test") == 0); char tooSmall[4] = {'x', 'x', 'x', 'x'}; Require(ScanListS("abcdef", "%s", tooSmall, 4u) == 0 && tooSmall[0] == '\0'); + CheckIntegerScanWidths(); std::puts("Formatting checks passed: 10000 iterations"); } @@ -286,31 +314,4 @@ static void CheckAllocatingFormat() { free_nid_postfix(allocated); } -static void CheckIntegerScanWidths() { - int value = 7; - *__error_nid_postfix() = 0; - Require(sscanf_nid_postfix("2147483648", "%d", &value) == 1 && value == -2147483647 - 1 && *__error_nid_postfix() == 0); - Require(sscanf_nid_postfix("0xFFFFFFFF", "%i", &value) == 1 && value == -1 && *__error_nid_postfix() == 0); - Require(sscanf_nid_postfix("99999999999999999999", "%d", &value) == 1 && value == -1 && *__error_nid_postfix() == 34); - unsigned unsignedValue = 7; - Require(sscanf_nid_postfix("4294967296", "%u", &unsignedValue) == 1 && unsignedValue == 0); - Require(sscanf_nid_postfix("100000001", "%x", &unsignedValue) == 1 && unsignedValue == 1); - Require(sscanf_nid_postfix("40000000001", "%o", &unsignedValue) == 1 && unsignedValue == 1); - short shortValue = 7; - Require(sscanf_nid_postfix("5000000000", "%hd", &shortValue) == 1 && shortValue == -3584); - signed char charValue = 7; - Require(sscanf_nid_postfix("300", "%hhd", &charValue) == 1 && charValue == 44); - long long quad = 7; - Require(sscanf_nid_postfix("3000000000", "%qd", &quad) == 1 && quad == 3000000000ll); - unsigned long long unsignedQuad = 7; - Require(sscanf_nid_postfix("ffffffffff", "%qx", &unsignedQuad) == 1 && unsignedQuad == 0xffffffffffull); - int first = 7; - int second = 7; - int position = 0; - Require(sscanf_nid_postfix("12 34", "%d %d%n", &first, &second, &position) == 2 && first == 12 && second == 34 && position == 5); - value = 7; - Require(sscanf_nid_postfix("x", "%d", &value) == 0 && value == 7); - Require(sscanf_nid_postfix("1 x", "%d %d", &first, &value) == 1 && first == 1 && value == 7); -} - -int main() { RunChecks(); CheckAllocatingFormat(); CheckIntegerScanWidths(); return CheckWidePrecision() ? 0 : 1; } +int main() { RunChecks(); CheckAllocatingFormat(); return CheckWidePrecision() ? 0 : 1; } From 8f9758fec02d9cba64f1f1ac6922ac5757a11b7a Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 13 Oct 2026 05:25:02 -0700 Subject: [PATCH 004/428] docs(libSceNet): list the inet text debt before sceNetShowNetstat #5124 adds an entry after sceNetShowNetstat at the end of the unknown function info list, so adding the inet entry there made the two pull requests conflict. --- docs/dev/debt/libSceNet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 733b160c3d..38f8a77a25 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -4,8 +4,8 @@ - PPSA01530 and PPSA13356 imports declared without parameters, signatures unknown, added without an implementation because the titles fail to load without them: [sceNetShowIfconfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet), [sce::Json::Value::setNullAccessCallBack](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), and [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad), whose real name is unknown. [_sceUltWaitingQueueResourcePoolCreate, _sceUltSemaphoreCreate, _sceUltMutexOptParamInitialize, _sceUltMutexCreate, _sceUltQueueDataResourcePoolCreate and _sceUltQueueCreate](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) forward to the exports without the underscore; that their argument lists match is inferred from the other underscore Ult exports, not measured. - [sceHttp2GetMemoryPoolStats, sceSslGetMemoryPoolStats, sceNetGetMemoryPoolStats](../../../core/libs/prx/libSceNet/Export.cpp) (libSceHttp2, libSceSsl, libSceNet) - the signatures `(int id, stats*)`, the layouts (pool size, peak and current use, and for Http2 and Ssl a 32-bit reserved field) and the error codes were measured on a PS5 (firmware 13.60). The pool size is the one given at init or pool creation; nothing is allocated from these pools, so the peak and current use are 0, where the console already reports use right after initialization (0x15C0 bytes for Ssl, 0x1640 for Http2 and 4 for a Net pool) -- [sceNetShowNetstat](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - imported by PPSA11386 and not called during its start-up; unknown signature, declared without parameters - [sceNetInetPton, sceNetInetNtop](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - convert the text with libkernel's [inet_pton and inet_ntop](../../../core/libs/prx/libkernel/Socket/src/AddressConversion.cpp), so they take the same FreeBSD 11.4 text forms, which are unverified on a console. The checks and errors before the conversion are libSceNet's own and unchanged +- [sceNetShowNetstat](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - imported by PPSA11386 and not called during its start-up; unknown signature, declared without parameters ### Functional From 3d1b4d460b70764d8e51272c2b018f74d6ed7a55 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:26:16 -0700 Subject: [PATCH 005/428] fix(libkernel): block a signal while its handler runs and honour SA_RESETHAND A guest signal handler ran with the mask it was raised under, so a handler that raised its own signal again re-entered itself at once, sigprocmask inside the handler did not report the signal or the handler's sa_mask as blocked, and SA_RESETHAND left the handler installed for the next delivery. FreeBSD 11.4 forms the handler's frame and then, in postsig_done() (sys/kern/kern_sig.c), blocks the handler's sa_mask and, unless SA_NODEFER is set, the signal itself, and resets the disposition to SIG_DFL when SA_RESETHAND is set; sigreturn restores the previous mask, so a signal raised meanwhile stays pending until the handler returns. sigaction keeps reporting SA_RESETHAND afterwards, since sigdflt() does not clear ps_sigreset. The handler now runs with its signal (unless SA_NODEFER) and its mask added to the guest mask, which is restored when it returns, and pending signals that the restored mask unblocks are delivered when raise returns. SA_RESETHAND resets the handler to the default before it runs. --- core/libs/prx/libkernel/CMakeLists.txt | 10 ++ .../libs/prx/libkernel/System/src/Signals.cpp | 53 ++++++++-- core/libs/tests/GuestSignalHandlerMask.cpp | 98 +++++++++++++++++++ 3 files changed, 153 insertions(+), 8 deletions(-) create mode 100644 core/libs/tests/GuestSignalHandlerMask.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index f3fb4d0806..1b62620e2a 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -624,6 +624,16 @@ if(BUILD_TESTING) set_tests_properties(guest_signals PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_signal_handler_mask_tests ${LIBS_INCLUDE_DIR}/tests/GuestSignalHandlerMask.cpp) + target_include_directories(guest_signal_handler_mask_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_signal_handler_mask_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_signal_handler_mask_tests) + add_test(NAME guest_signal_handler_mask COMMAND guest_signal_handler_mask_tests) + set_tests_properties(guest_signal_handler_mask PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_signal_handler_mask PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_sigtimedwait_tests ${LIBS_INCLUDE_DIR}/tests/GuestSigtimedwait.cpp) target_include_directories(guest_sigtimedwait_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_sigtimedwait_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 01226d305d..bcfa89969e 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -38,8 +38,13 @@ constexpr int MaxSignal = 128; constexpr int GuestSigkill = 9; constexpr int GuestSigstop = 17; constexpr int SaRestart = 0x2; +constexpr int SaResethand = 0x4; +constexpr int SaNodefer = 0x10; constexpr int SaSiginfo = 0x40; std::atomic handlers[MaxSignal + 1]{}; +std::atomic handlerMasks[MaxSignal + 1]{}; +std::atomic handlerFlags[MaxSignal + 1]{}; +std::atomic handlerReset[MaxSignal + 1]{}; static_assert(std::atomic::is_always_lock_free); std::atomic blockedMask{0}; static_assert(std::atomic::is_always_lock_free); @@ -138,6 +143,20 @@ int NativeSignal(int guest) { default: return 0; } } +void ResetToDefault(int guest) { + handlerReset[guest].store(true); + handlers[guest].store(nullptr); + if (const int native = NativeSignal(guest)) std::signal(native, SIG_DFL); +} +void RunHandler(int guest, GuestHandler handler) { + const int flags = handlerFlags[guest].load(); + std::uint32_t mask = handlerMasks[guest].load(); + if ((flags & SaNodefer) == 0 && guest <= 32) mask |= SignalBit(guest); + if ((flags & SaResethand) != 0) ResetToDefault(guest); + const std::uint32_t saved = blockedMask.fetch_or(mask); + handler(guest); + blockedMask.store(saved); +} void Dispatch(int native) { int guest = 0; for (int candidate : {2, 4, 6, 8, 11, 15}) @@ -152,12 +171,15 @@ void Dispatch(int native) { return; } const auto callback = handlers[guest].load(); - if (reinterpret_cast(callback) > 1) callback(guest); + if (reinterpret_cast(callback) > 1) RunHandler(guest, callback); } bool Delivered(const GuestSigaction& action) { return action.handler > 1 && (action.flags & SaSiginfo) == 0; } bool Install(int guest, const GuestSigaction& action) { + handlerReset[guest].store(false); + handlerMasks[guest].store(action.mask.bits[0]); + handlerFlags[guest].store(action.flags); const int native = NativeSignal(guest); if (!native) return true; const auto previous = handlers[guest].exchange(Delivered(action) ? reinterpret_cast(action.handler) : nullptr); @@ -172,8 +194,15 @@ bool Install(int guest, const GuestSigaction& action) { namespace { +void SettleReset(int guest) { + if (!handlerReset[guest].exchange(false)) return; + dispositions[guest].handler = 0; + dispositions[guest].flags &= ~SaSiginfo; +} + GuestSigaction Disposition(int guest) { std::lock_guard lock(registration); + SettleReset(guest); return dispositions[guest]; } @@ -205,12 +234,19 @@ bool DeliverPendingSignal(int guest) { return action.handler > 1; } if (action.handler > 1) { - reinterpret_cast(action.handler)(guest); + RunHandler(guest, reinterpret_cast(action.handler)); return true; } return false; } +void DeliverUnblockedPending() { + for (int guest = 1; guest <= MaxSignal; ++guest) { + if (SignalBlocked(guest) || !Deliverable(guest) || !TakePending(guest)) continue; + DeliverPendingSignal(guest); + } +} + int WaitForPendingInSet(const GuestSignalSet& set, bool waitForever, const GuestTimespec* timeout, int* guest) { auto& pending = Pending(); std::unique_lock lock(pending.lock); @@ -435,6 +471,7 @@ GuestHandler APS5_VABI signal_nid_postfix(int guest, GuestHandler handler) { const auto invalid = reinterpret_cast(static_cast(-1)); if (guest < 1 || guest > MaxSignal || ((guest == GuestSigkill || guest == GuestSigstop) && handler != nullptr)) { *__error_nid_postfix() = 22; return invalid; } std::lock_guard lock(registration); + SettleReset(guest); const GuestSigaction action{reinterpret_cast(handler), SaRestart, {}}; if (!Install(guest, action)) { *__error_nid_postfix() = 22; return invalid; } const auto previous = reinterpret_cast(dispositions[guest].handler); @@ -445,6 +482,7 @@ int APS5_VABI sigaction_nid_postfix(int guest, const GuestSigaction* action, Gue if (guest < 1 || guest > MaxSignal) { *__error_nid_postfix() = 22; return -1; } if (action && (guest == GuestSigkill || guest == GuestSigstop) && action->handler != 0) { *__error_nid_postfix() = 22; return -1; } std::lock_guard lock(registration); + SettleReset(guest); const GuestSigaction old = dispositions[guest]; if (action) { if (!Install(guest, *action)) { *__error_nid_postfix() = 22; return -1; } @@ -464,11 +502,13 @@ int APS5_VABI raise_nid_postfix(int guest) { const int native = NativeSignal(guest); if (native != 0) { const int result = std::raise(native); - if (result) *__error_nid_postfix() = 22; - return result ? -1 : 0; + if (result) { *__error_nid_postfix() = 22; return -1; } + DeliverUnblockedPending(); + return 0; } if (!CaughtByGuest(guest)) { *__error_nid_postfix() = 22; return -1; } DeliverPendingSignal(guest); + DeliverUnblockedPending(); return 0; } int APS5_VABI kill_nid_postfix(int pid, int guest) { @@ -520,10 +560,7 @@ int APS5_VABI _sigprocmask_nid_postfix(int how, const GuestSignalSet* set, Guest } } } - for (int guest = 1; guest <= MaxSignal; ++guest) { - if (SignalBlocked(guest) || !Deliverable(guest) || !TakePending(guest)) continue; - DeliverPendingSignal(guest); - } + DeliverUnblockedPending(); return 0; } diff --git a/core/libs/tests/GuestSignalHandlerMask.cpp b/core/libs/tests/GuestSignalHandlerMask.cpp new file mode 100644 index 0000000000..5ef9ecfd2a --- /dev/null +++ b/core/libs/tests/GuestSignalHandlerMask.cpp @@ -0,0 +1,98 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sigaction_nid_postfix(int, const void*, void*); +int APS5_VABI sigprocmask_nid_postfix(int, const void*, void*); +int APS5_VABI raise_nid_postfix(int); +} + +struct GuestSignalSet { + std::uint32_t bits[4]; +}; + +struct GuestSigaction { + std::uintptr_t handler; + int flags; + GuestSignalSet mask; +}; + +static constexpr int SigTerm = 15; +static constexpr int SigUsr1 = 30; +static constexpr int SigUsr2 = 31; +static constexpr int SaRestart = 0x2; +static constexpr int SaResethand = 0x4; +static constexpr int SaNodefer = 0x10; +static constexpr int SigBlock = 1; + +static std::uint32_t Bit(int signal) { return 1u << (signal - 1); } + +static void Require(bool value) { if (!value) std::abort(); } + +static int calls = 0; +static int callsAfterNestedRaise = 0; +static std::uint32_t maskInHandler = 0; + +static std::uint32_t CurrentMask() { + GuestSignalSet set{}; + Require(sigprocmask_nid_postfix(SigBlock, nullptr, &set) == 0); + return set.bits[0]; +} + +static void APS5_VABI RaiseAgain(int signal) { + if (++calls == 1) { + maskInHandler = CurrentMask(); + Require(raise_nid_postfix(signal) == 0); + callsAfterNestedRaise = calls; + } +} + +static void APS5_VABI Count(int) { + ++calls; +} + +static void Install(int signal, void (APS5_VABI* handler)(int), int flags, std::uint32_t mask) { + GuestSigaction action{reinterpret_cast(handler), flags, {{mask, 0, 0, 0}}}; + Require(sigaction_nid_postfix(signal, &action, nullptr) == 0); +} + +static void CheckDeferred(int signal) { + calls = 0; + callsAfterNestedRaise = 0; + Install(signal, RaiseAgain, SaRestart, Bit(SigUsr1)); + Require(raise_nid_postfix(signal) == 0); + Require(callsAfterNestedRaise == 1); + Require(calls == 2); + Require(maskInHandler == (Bit(signal) | Bit(SigUsr1))); + Require(CurrentMask() == 0); +} + +static void CheckNodefer() { + calls = 0; + callsAfterNestedRaise = 0; + Install(SigUsr2, RaiseAgain, SaRestart | SaNodefer, 0); + Require(raise_nid_postfix(SigUsr2) == 0); + Require(callsAfterNestedRaise == 2); + Require(maskInHandler == 0); + Require(CurrentMask() == 0); +} + +static void CheckResethand() { + calls = 0; + Install(SigUsr1, Count, SaResethand, 0); + Require(raise_nid_postfix(SigUsr1) == 0); + Require(calls == 1); + GuestSigaction current{}; + Require(sigaction_nid_postfix(SigUsr1, nullptr, ¤t) == 0); + Require(current.handler == 0); + Require((current.flags & SaResethand) != 0); +} + +int main() { + CheckDeferred(SigUsr2); + CheckDeferred(SigTerm); + CheckNodefer(); + CheckResethand(); + return 0; +} From ebb86e4c2e8c3b22c28316928af369d333716a4e Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:47:28 -0700 Subject: [PATCH 006/428] docs(libkernel): record the handler mask and SA_RESETHAND for sigaction The sigaction entry said that the mask and flags of a running handler are not applied. It now says what is: the signal itself unless SA_NODEFER, and the first 32 signals of sa_mask, are added to the process-wide mask while the handler runs, and SA_RESETHAND resets the disposition first. It also notes what is not: SA_ONSTACK, and the delivery of a signal left pending by a handler that a host signal ran asynchronously, which waits for the next raise or _sigprocmask. --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index b54b27aba9..44c450c7b3 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -153,7 +153,7 @@ - [sigtimedwait](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) waits on a process-wide pending set instead of throwing: a signal raised while blocked stays pending and is taken by `sigwait`, `sigwaitinfo` or `sigtimedwait`, delivered to the handler when `_sigprocmask` unblocks it, or delivered by `pause`. The set covers all 128 signals and the lowest number that is pending and in the set is selected (FreeBSD specifies the lowest number only among real-time signals). Selection, `EAGAIN` on timeout, `EINVAL` for a `tv_nsec` outside 0 to 1000000000 while nothing is pending and `NULL` timeout blocking indefinitely follow FreeBSD's [sigtimedwait(2)](https://man.freebsd.org/cgi/man.cgi?query=sigtimedwait). `sigwait` returns 0 with the number in its output, as POSIX specifies, and `sigwaitinfo` and `sigtimedwait` return the number, as FreeBSD does. The guest `siginfo_t` follows FreeBSD 11's `sys/sys/siginfo.h`: 80 bytes, `si_signo` at 0, `si_code` at 8, `si_pid` at 12, `si_uid` at 16, `si_addr` at 24, `si_value` at 32 and `si_band` at 40; only `si_signo`, `si_code` (`SI_USER`) and `si_pid` (the guest pid) are written, `si_uid` stays 0 because no uid is modelled, and the console's layout is not verified. PPSA85300 imports `sigtimedwait` and fails to load without it; its only call site is VLC's `vlc_writev` ([filesystem.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/posix/filesystem.c#L243-L278)), which polls with a zero timeout to drain a `SIGPIPE` only after `writev` fails with `EPIPE`. The waits are not cancellation points and an unblocked caught signal does not interrupt them with `EINTR`, where FreeBSD's `sigwaitinfo` and `sigtimedwait` may return it. `SIGPIPE` has no host equivalent and is never raised by a host write here (see `read, write and vector I/O`), so VLC's drain finds nothing - [pause, sigwait, sigwaitinfo](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) take the pending set of `sigtimedwait` instead of throwing. `pause` waits for a pending signal that is not blocked and whose disposition can be delivered, runs its handler on the calling thread and returns -1 with `EINTR`; an ignored signal is discarded and the wait continues, and it never returns for a signal whose handler cannot run. A signal whose registered handler has `SA_SIGINFO` still throws, and one whose disposition is the default action and that has no host equivalent (every signal except 2, 4, 6, 8, 11 and 15) is neither delivered nor discarded: it stays pending until a wait takes it, where FreeBSD runs the default action, which terminates the process for most of them. PPSA68091, PPSA99169, PPSA99420 and PPSA97358 import them and fail to load without them. PPSA68091 calls `pause` at one site (0x8F7266), in a loop that waits while a global holds another thread's `errno` address. PPSA99169 only stores the address of `pause` in a table of symbol addresses (0x30DB1). PPSA99420 calls `pause` at 0x3BD80F6 and 0x3C9E220, `sigwait` at 0x3C9E4C7 and `sigwaitinfo` at 0x3C9E588. PPSA97358 calls `sigwait` from QEMU's `sigwait_compat` thread ([compatfd.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/compatfd.c#L28-L53)), which `qemu_signal_init` ([main-loop.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/main-loop.c#L110-L147)) starts for `SIGIO`, `SIGALRM` and `SIGBUS` when the main loop is created, so it reaches the wait at start-up, and from the vCPU thread of the dummy accelerator ([dummy-cpus.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/accel/dummy-cpus.c#L48-L56)). None of the four waits is a cancellation point - [getsid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns the calling process ID for pid 0 and the calling process, and `ESRCH` for other process IDs, assuming the single guest process is its own session leader. PPSA74126 imports it and fails to load without it -- [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating +- [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do. While a handler runs, its own signal (unless `SA_NODEFER`) and the first 32 signals of its mask are added to the process-wide mask, which is restored when it returns, and `SA_RESETHAND` resets the disposition to the default before it runs, as FreeBSD's `postsig_done` does; a signal raised meanwhile stays pending and is delivered when the handler returns to `raise`, but one left pending by a handler that a host signal ran asynchronously waits for the next `raise` or `_sigprocmask`. `SA_ONSTACK` is not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating - [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console - [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `EINVAL` for every other command, `F_GETFD` and `F_SETFD` included). The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux, where `F_GETLK` with `F_UNLCK` throws. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console From a2b849e10c3bcca45d4d7c7deb833678c36024ab Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:04:51 -0700 Subject: [PATCH 007/428] fix(libSceNet): report a stream whose peer closed as readable on Windows When the peer of a stream socket closes, Winsock's WSAPoll reports POLLHUP without POLLRDNORM, whether or not the socket's own sending side is shut down. sceNetEpollWait passed that on, so a socket whose peer had closed came back as SCE_NET_EPOLLHUP alone and never as readable: a loop that waits for SCE_NET_EPOLLIN to read the end of the stream never got it, while every wait returned at once. FreeBSD's sopoll_generic reports a socket that can receive no more as readable, and adds POLLHUP only once it can send no more either. Linux does the same, so the Linux path already behaved this way. On Windows, a stream socket's POLLHUP without POLLERR now reports POLLRDNORM when reading was asked for, and keeps POLLHUP only after sceNetShutdown has shut the socket's own sending side, which libSceNet now records. --- core/libs/prx/libSceNet/Export.cpp | 27 +++++++++++++++++++----- core/libs/tests/GuestSceNet.cpp | 34 ++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 7c34334844..a62c63a28f 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -241,6 +241,7 @@ struct Sock { bool bound = false; bool listening = false; bool aborted = false; + bool sending_shut = false; int rcv_timeout_us = 0; int snd_timeout_us = 0; }; @@ -445,6 +446,14 @@ int timeout_option(int optname) { return optname; } +#ifdef _WIN32 +short closed_stream_events(const Sock& socket, short native, short requested) { + if (socket.type != NET_SOCK_STREAM || (native & (POLLHUP | POLLERR)) != POLLHUP) return native; + native = static_cast(native | (requested & POLLRDNORM)); + return socket.sending_shut ? native : static_cast(native & ~POLLHUP); +} +#endif + std::int64_t message_length(const NetMsghdr* message) { if (!message) return fail(NET_EFAULT); if (message->iov_length < 0 || message->iov_length > NET_UIO_MAXIOV) return fail(NET_EMSGSIZE); @@ -939,7 +948,13 @@ int APS5_VABI sceNetShutdown(int s, int how) { if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } - return ::shutdown(socket.native->value, how) == 0 ? 0 : fail(native_error()); + if (::shutdown(socket.native->value, how) != 0) return fail(native_error()); + if (how != 0) { + std::lock_guard lk(g_mutex); + const auto it = g_socks.find(s); + if (it != g_socks.end()) it->second.sending_shut = true; + } + return 0; } int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval, uint32_t optlen) { @@ -1129,7 +1144,7 @@ int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int : std::chrono::steady_clock::time_point::max(); for (;;) { std::vector registered_events; - std::vector> native_sockets; + std::vector sockets; { std::lock_guard lk(g_mutex); if (g_epolls.count(eid) == 0) return fail(NET_EBADF); @@ -1140,7 +1155,7 @@ int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int const auto socket = g_socks.find(entry.first); if (socket == g_socks.end()) continue; registered_events.push_back({entry.second.events, 0, static_cast(entry.first), entry.second.data}); - native_sockets.push_back(socket->second.native); + sockets.push_back(socket->second); } } } @@ -1158,14 +1173,14 @@ int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int #ifdef _WIN32 std::vector descriptors(registered_events.size()); for (std::size_t index = 0; index < descriptors.size(); ++index) { - descriptors[index].fd = native_sockets[index]->value; + descriptors[index].fd = sockets[index].native->value; if (registered_events[index].events & 1) descriptors[index].events |= POLLRDNORM; if (registered_events[index].events & 2) descriptors[index].events |= POLLWRNORM; } #else std::vector descriptors(registered_events.size()); for (std::size_t index = 0; index < descriptors.size(); ++index) { - descriptors[index].fd = native_sockets[index]->value; + descriptors[index].fd = sockets[index].native->value; if (registered_events[index].events & 1) descriptors[index].events |= POLLIN; if (registered_events[index].events & 2) descriptors[index].events |= POLLOUT; } @@ -1180,6 +1195,8 @@ int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int } #ifdef _WIN32 const int ready_count = WSAPoll(descriptors.data(), static_cast(descriptors.size()), wait_ms); + for (std::size_t index = 0; index < descriptors.size(); ++index) + descriptors[index].revents = closed_stream_events(sockets[index], descriptors[index].revents, descriptors[index].events); #else const int ready_count = ::poll(descriptors.data(), descriptors.size(), wait_ms); #endif diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index e88dc22a86..8164147098 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -447,6 +447,39 @@ static void CheckEpollIdent() { Require(sceNetSocketClose(sender) == 0 && sceNetSocketClose(receiver) == 0); } +static void CheckEpollClosedStream() { + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + const int listener = sceNetSocket("closed-stream-listener", 2, 1, 6); + Require(listener >= 0); + Require(sceNetBind_nid_postfix(listener, address.data(), address.size()) == 0 && sceNetListen(listener, 1) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(listener, address.data(), &size) == 0); + const int client = sceNetSocket("closed-stream-client", 2, 1, 6); + Require(client >= 0 && sceNetConnect(client, address.data(), address.size()) == 0); + const int accepted = sceNetAccept(listener, nullptr, nullptr); + Require(accepted >= 0); + const int epoll = sceNetEpollCreate("epoll-closed-stream", 0); + Require(epoll >= 0); + NetEpollEvent registration{}; + registration.events = 1; + registration.ident = static_cast(accepted); + Require(sceNetEpollControl(epoll, 1, accepted, ®istration) == 0); + Require(sceNetSocketClose(client) == 0); + NetEpollEvent ready{}; + Require(sceNetEpollWait(epoll, &ready, 1, 1000000) == 1 && ready.events == 1); + char byte = 0; + Require(sceNetRecv(accepted, &byte, 1, 0) == 0); + registration.events = 3; + Require(sceNetEpollControl(epoll, 2, accepted, ®istration) == 0); + Require(sceNetEpollWait(epoll, &ready, 1, 0) == 1 && ready.events == 3); + Require(sceNetShutdown(accepted, 1) == 0); + registration.events = 1; + Require(sceNetEpollControl(epoll, 2, accepted, ®istration) == 0); + Require(sceNetEpollWait(epoll, &ready, 1, 0) == 1 && ready.events == (1 | 16)); + Require(sceNetEpollDestroy(epoll) == 0); + Require(sceNetSocketClose(accepted) == 0 && sceNetSocketClose(listener) == 0); +} + static bool ipv6Unavailable = false; static bool HostIpv6LoopbackUnavailable() { @@ -654,6 +687,7 @@ int main() { CheckAddressText(28, "::1"); CheckAddressText(28, "1234:5678:9abc:def0:1234:5678:9abc:def0"); CheckEpollIdent(); + CheckEpollClosedStream(); CheckLinkLocalAllNodesIpv6(); CheckUnboundSocketName(); CheckUnspecifiedIpv6(); From 52d6cb1d2340b110b613ed85c0565898661e7841 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:04:45 -0700 Subject: [PATCH 008/428] fix(libkernel): resolve original .prx names to relinked .guest.prx in lstat #4688 made stat, sceKernelStat, read-only open and sceKernelCheckReachability find X.guest.prx when a title asks for X, the name the relinker replaced. lstat still answered ENOENT for the same path, so a title that stats a plugin with lstat saw it missing while stat and dlopen found it. lstat now resolves the name the same way; an existing X, including a dangling symlink, is still reported as is. --- core/libs/prx/libkernel/File/src/Stdio.cpp | 1 + core/libs/tests/GuestFilesystem.cpp | 2 ++ 2 files changed, 3 insertions(+) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index d9ceb8756b..28d8addf6f 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -787,6 +787,7 @@ int APS5_VABI lstat_nid_postfix(const char* path, FileStat* sb) { if (const int error = PathError(path)) return PosixFailure(error); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); + native = File::PathOrRelinkedModule(native); if (!File::FillLinkStat(native, sb)) return PosixResult(SceErrorFromErrno(errno)); return 0; } diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 987e09890d..26880c11c2 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -183,11 +183,13 @@ static void TestRelinkedModuleNames() { const auto relinked = original + ".guest.prx"; FileStat status{}; Require(sceKernelStat(original.c_str(), &status) == static_cast(0x80020002u)); + Require(lstat_nid_postfix(original.c_str(), &status) == -1 && *__error_nid_postfix() == 2); Require(sceKernelCheckReachability(original.c_str()) == static_cast(0x80020002u)); Require(sceKernelOpen(original.c_str(), 0, 0) == static_cast(0x80020002u)); { std::ofstream file(relinked); file << "relinked"; } Require(sceKernelStat(original.c_str(), &status) == 0 && status.st_size == 8); Require(stat_nid_postfix(original.c_str(), &status) == 0 && status.st_size == 8); + Require(lstat_nid_postfix(original.c_str(), &status) == 0 && status.st_size == 8); Require(sceKernelCheckReachability(original.c_str()) == 0); const int opened = sceKernelOpen(original.c_str(), 0, 0); Require(opened >= 0 && sceKernelClose(opened) == 0); From 4d35b48dafc7432c3136c53d02aacefddde646ea Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:08:21 -0700 Subject: [PATCH 009/428] fix(libkernel): stop Winsock failing an unconnected datagram receive after an unreachable port Winsock reports an ICMP port-unreachable reply as WSAECONNRESET on the next receive of any datagram socket, so on Windows a guest UDP socket that sent to a port nobody listens on failed its next recvfrom with ECONNRESET. FreeBSD's udp_common_ctlinput passes the reply on only to a socket connected to the sender; an unconnected one keeps receiving. socket now turns SIO_UDP_CONNRESET off on datagram sockets on Windows, as #5295 does for sceNetSocket. --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 8 ++++++ core/libs/tests/GuestUdp.cpp | 29 ++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 7a0304ce10..f648525a9e 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -2,6 +2,7 @@ #define WIN32_LEAN_AND_MEAN #include #include +#include #include #include #else @@ -770,6 +771,13 @@ int APS5_VABI socket_nid_postfix(int family, int type, int protocol) { const int ipv6Only = 1; if (family == 28 && ::setsockopt(native, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast(&ipv6Only), sizeof(ipv6Only))) return Fail(NativeError()); +#ifdef _WIN32 + BOOL reportUnreachable = FALSE; + DWORD returned = 0; + if (nativeType == SOCK_DGRAM && WSAIoctl(native, SIO_UDP_CONNRESET, &reportUnreachable, sizeof(reportUnreachable), nullptr, 0, + &returned, nullptr, nullptr) != 0) + return Fail(NativeError()); +#endif try { auto socket = std::make_shared(native, family, plainType); guard.value = Invalid; diff --git a/core/libs/tests/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp index c1b4db8b80..6a51c10908 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -47,6 +47,34 @@ std::int64_t APS5_VABI sceKernelWritev(int, const Iovec*, int); } static void Require(bool value) { if (!value) std::abort(); } +static void CheckUnreachablePortIgnored() { + const int closing = socket_nid_postfix(2, 2, 0); + Require(closing >= 0); + std::array unreachable{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(closing, unreachable.data(), unreachable.size()) == 0); + std::uint32_t size = unreachable.size(); + Require(getsockname_nid_postfix(closing, unreachable.data(), &size) == 0 && size == unreachable.size()); + const int receiver = socket_nid_postfix(2, 2 | 0x20000000, 0); + const int sender = socket_nid_postfix(2, 2, 0); + Require(receiver >= 0 && sender >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(receiver, address.data(), address.size()) == 0); + size = address.size(); + Require(getsockname_nid_postfix(receiver, address.data(), &size) == 0); + Require(close_nid_postfix(closing) == 0); + Require(sendto_nid_postfix(receiver, "x", 1, 0, unreachable.data(), unreachable.size()) == 1); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + char received[4]{}; + for (int attempt = 0; attempt < 3; ++attempt) { + Require(recvfrom_nid_postfix(receiver, received, sizeof(received), 0, nullptr, nullptr) == -1 && *__error_nid_postfix() == 35); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } + Require(sendto_nid_postfix(sender, "ok", 2, 0, address.data(), address.size()) == 2); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(recvfrom_nid_postfix(receiver, received, sizeof(received), 0, nullptr, nullptr) == 2 && received[0] == 'o'); + Require(close_nid_postfix(sender) == 0 && close_nid_postfix(receiver) == 0); +} + static void CheckAddressLengths(int family) { const std::uint32_t addressSize = family == 2 ? 16 : 28; const int receiver = socket_nid_postfix(family, 2, 0); @@ -276,4 +304,5 @@ int main() { Require(closeOnExec >= 0 && fcntl_nid_postfix(closeOnExec, 3) == 2); Require(close_nid_postfix(closeOnExec) == 0); Require(socket_nid_postfix(2, 0x20000000, 0) == -1 && *__error_nid_postfix() == 43); + CheckUnreachablePortIgnored(); } From f4cfb75479eb3f9f97161e1fb0d2e37ef76f0642 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:08:26 -0700 Subject: [PATCH 010/428] docs(libkernel): record that datagram sockets ignore ICMP port-unreachable on Windows With SIO_UDP_CONNRESET off, a datagram socket connected to a port nobody listens on no longer fails its next send or receive at all on Windows, where FreeBSD fails it with ECONNREFUSED. --- docs/dev/debt/libkernel.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 637ab5eb36..f89845a54b 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -138,6 +138,7 @@ - [hw.model](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the host CPU's name, built as FreeBSD 11 builds `hw.model`: the CPUID brand string (leaves 0x80000002 to 0x80000004) without leading spaces ([identcpu.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/identcpu.c#L683-L691)). Trailing padding stays, as it does there, so an AMD host's name ends in spaces. The name a PS5 reports is unknown. A write returns -1 with `EPERM`. A host without an x86 CPUID brand string, such as an Apple Silicon Mac, throws. PPSA99203 only prints it (`[console] hw.model: %s`) - [getrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `RLIMIT_CPU`, `RLIMIT_FSIZE`, `RLIMIT_CORE`, `RLIMIT_SBSIZE`, `RLIMIT_VMEM`/`AS`, `RLIMIT_NPTS`, `RLIMIT_SWAP`, `RLIMIT_KQUEUES` and `RLIMIT_UMTXP` report infinity as in FreeBSD's process-0 defaults, and `RLIMIT_DATA` mirrors the host limit. `RLIMIT_STACK`, `RLIMIT_RSS`, `RLIMIT_MEMLOCK`, `RLIMIT_NPROC` and `RLIMIT_NOFILE` throw because no console values are established. - [setrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) validates the resource and input in FreeBSD order, maps negative limits to infinity, and lowers a soft limit above the hard limit. A request succeeds only when its normalized values match `getrlimit`; changing limits throws because guest limit state is not stored. +- [socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) turns off Winsock's report of an ICMP port-unreachable reply on datagram sockets (`SIO_UDP_CONNRESET`). Winsock fails the next receive on any datagram socket with `WSAECONNRESET`, where FreeBSD's [`udp_common_ctlinput`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/netinet/udp_usrreq.c#L774-L798) reports the reply only to a socket connected to the sender. So on Windows a datagram socket connected to a port nobody listens on does not fail its next send or receive with `ECONNREFUSED` either - [execvp](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported. PPSA85300 imports it and fails to load without it; its `fork` and `vfork` fail with `ENOSYS` ([libc_missing.c](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/ps5/compat/libc_missing.c#L290-L292)), so the child process that would call it is never created. - [socketpair](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and a stream `send` after the peer closed reports `ECONNABORTED` instead of the console's `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLHUP` without `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, where the console reports `POLLIN`, with `POLLHUP` only after `close` - [UDP recv/recvfrom](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) and [sceNetRecv/sceNetRecvfrom](../../../core/libs/prx/libSceNet/Export.cpp) translate Windows WSAEMSGSIZE into the copied byte count for datagram sockets, including MSG_PEEK, and retain the sender address. This follows FreeBSD 11.4 recv(2) and Winsock's documented truncated-datagram behavior; console behavior and existing title impact are unverified. From ef4590484296d7b76fa827ad4a4b24561a946b7a Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:26:47 -0700 Subject: [PATCH 011/428] fix(libkernel): unblock only the signals a handler added when it returns The blocked mask is still process-wide, so restoring the whole mask saved before the handler undid any change another thread made while it ran. --- .../libs/prx/libkernel/System/src/Signals.cpp | 4 ++-- core/libs/tests/GuestSignalHandlerMask.cpp | 22 +++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index bcfa89969e..b20877c19e 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -153,9 +153,9 @@ void RunHandler(int guest, GuestHandler handler) { std::uint32_t mask = handlerMasks[guest].load(); if ((flags & SaNodefer) == 0 && guest <= 32) mask |= SignalBit(guest); if ((flags & SaResethand) != 0) ResetToDefault(guest); - const std::uint32_t saved = blockedMask.fetch_or(mask); + const std::uint32_t added = mask & ~blockedMask.fetch_or(mask); handler(guest); - blockedMask.store(saved); + blockedMask.fetch_and(~added); } void Dispatch(int native) { int guest = 0; diff --git a/core/libs/tests/GuestSignalHandlerMask.cpp b/core/libs/tests/GuestSignalHandlerMask.cpp index 5ef9ecfd2a..332bbbbb21 100644 --- a/core/libs/tests/GuestSignalHandlerMask.cpp +++ b/core/libs/tests/GuestSignalHandlerMask.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include extern "C" { int APS5_VABI sigaction_nid_postfix(int, const void*, void*); @@ -25,6 +26,7 @@ static constexpr int SaRestart = 0x2; static constexpr int SaResethand = 0x4; static constexpr int SaNodefer = 0x10; static constexpr int SigBlock = 1; +static constexpr int SigUnblock = 2; static std::uint32_t Bit(int signal) { return 1u << (signal - 1); } @@ -52,6 +54,14 @@ static void APS5_VABI Count(int) { ++calls; } +static void APS5_VABI BlockFromOtherThread(int) { + ++calls; + std::thread([] { + const GuestSignalSet set{{Bit(SigTerm), 0, 0, 0}}; + Require(sigprocmask_nid_postfix(SigBlock, &set, nullptr) == 0); + }).join(); +} + static void Install(int signal, void (APS5_VABI* handler)(int), int flags, std::uint32_t mask) { GuestSigaction action{reinterpret_cast(handler), flags, {{mask, 0, 0, 0}}}; Require(sigaction_nid_postfix(signal, &action, nullptr) == 0); @@ -89,10 +99,22 @@ static void CheckResethand() { Require((current.flags & SaResethand) != 0); } +static void CheckOtherThreadKept() { + calls = 0; + Install(SigUsr1, BlockFromOtherThread, SaRestart, 0); + Require(raise_nid_postfix(SigUsr1) == 0); + Require(calls == 1); + Require(CurrentMask() == Bit(SigTerm)); + const GuestSignalSet set{{Bit(SigTerm), 0, 0, 0}}; + Require(sigprocmask_nid_postfix(SigUnblock, &set, nullptr) == 0); + Require(CurrentMask() == 0); +} + int main() { CheckDeferred(SigUsr2); CheckDeferred(SigTerm); CheckNodefer(); CheckResethand(); + CheckOtherThreadKept(); return 0; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 44c450c7b3..576d1c5a67 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -153,7 +153,7 @@ - [sigtimedwait](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) waits on a process-wide pending set instead of throwing: a signal raised while blocked stays pending and is taken by `sigwait`, `sigwaitinfo` or `sigtimedwait`, delivered to the handler when `_sigprocmask` unblocks it, or delivered by `pause`. The set covers all 128 signals and the lowest number that is pending and in the set is selected (FreeBSD specifies the lowest number only among real-time signals). Selection, `EAGAIN` on timeout, `EINVAL` for a `tv_nsec` outside 0 to 1000000000 while nothing is pending and `NULL` timeout blocking indefinitely follow FreeBSD's [sigtimedwait(2)](https://man.freebsd.org/cgi/man.cgi?query=sigtimedwait). `sigwait` returns 0 with the number in its output, as POSIX specifies, and `sigwaitinfo` and `sigtimedwait` return the number, as FreeBSD does. The guest `siginfo_t` follows FreeBSD 11's `sys/sys/siginfo.h`: 80 bytes, `si_signo` at 0, `si_code` at 8, `si_pid` at 12, `si_uid` at 16, `si_addr` at 24, `si_value` at 32 and `si_band` at 40; only `si_signo`, `si_code` (`SI_USER`) and `si_pid` (the guest pid) are written, `si_uid` stays 0 because no uid is modelled, and the console's layout is not verified. PPSA85300 imports `sigtimedwait` and fails to load without it; its only call site is VLC's `vlc_writev` ([filesystem.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/posix/filesystem.c#L243-L278)), which polls with a zero timeout to drain a `SIGPIPE` only after `writev` fails with `EPIPE`. The waits are not cancellation points and an unblocked caught signal does not interrupt them with `EINTR`, where FreeBSD's `sigwaitinfo` and `sigtimedwait` may return it. `SIGPIPE` has no host equivalent and is never raised by a host write here (see `read, write and vector I/O`), so VLC's drain finds nothing - [pause, sigwait, sigwaitinfo](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) take the pending set of `sigtimedwait` instead of throwing. `pause` waits for a pending signal that is not blocked and whose disposition can be delivered, runs its handler on the calling thread and returns -1 with `EINTR`; an ignored signal is discarded and the wait continues, and it never returns for a signal whose handler cannot run. A signal whose registered handler has `SA_SIGINFO` still throws, and one whose disposition is the default action and that has no host equivalent (every signal except 2, 4, 6, 8, 11 and 15) is neither delivered nor discarded: it stays pending until a wait takes it, where FreeBSD runs the default action, which terminates the process for most of them. PPSA68091, PPSA99169, PPSA99420 and PPSA97358 import them and fail to load without them. PPSA68091 calls `pause` at one site (0x8F7266), in a loop that waits while a global holds another thread's `errno` address. PPSA99169 only stores the address of `pause` in a table of symbol addresses (0x30DB1). PPSA99420 calls `pause` at 0x3BD80F6 and 0x3C9E220, `sigwait` at 0x3C9E4C7 and `sigwaitinfo` at 0x3C9E588. PPSA97358 calls `sigwait` from QEMU's `sigwait_compat` thread ([compatfd.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/compatfd.c#L28-L53)), which `qemu_signal_init` ([main-loop.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/main-loop.c#L110-L147)) starts for `SIGIO`, `SIGALRM` and `SIGBUS` when the main loop is created, so it reaches the wait at start-up, and from the vCPU thread of the dummy accelerator ([dummy-cpus.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/accel/dummy-cpus.c#L48-L56)). None of the four waits is a cancellation point - [getsid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns the calling process ID for pid 0 and the calling process, and `ESRCH` for other process IDs, assuming the single guest process is its own session leader. PPSA74126 imports it and fails to load without it -- [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do. While a handler runs, its own signal (unless `SA_NODEFER`) and the first 32 signals of its mask are added to the process-wide mask, which is restored when it returns, and `SA_RESETHAND` resets the disposition to the default before it runs, as FreeBSD's `postsig_done` does; a signal raised meanwhile stays pending and is delivered when the handler returns to `raise`, but one left pending by a handler that a host signal ran asynchronously waits for the next `raise` or `_sigprocmask`. `SA_ONSTACK` is not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating +- [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do. While a handler runs, its own signal (unless `SA_NODEFER`) and the first 32 signals of its mask are added to the process-wide mask, and `SA_RESETHAND` resets the disposition to the default before it runs, as FreeBSD's `postsig_done` does. When the handler returns, only the signals it added are unblocked again, so a mask change another thread made meanwhile is kept, but so is one the handler made itself, which FreeBSD's `sigreturn` would undo; a signal raised meanwhile stays pending and is delivered when the handler returns to `raise`, but one left pending by a handler that a host signal ran asynchronously waits for the next `raise` or `_sigprocmask`. `SA_ONSTACK` is not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating - [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console - [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `EINVAL` for every other command, `F_GETFD` and `F_SETFD` included). The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux, where `F_GETLK` with `F_UNLCK` throws. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console From 624815b2004c8d86a20ccd06ad52acf1baab1184 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:45:47 -0700 Subject: [PATCH 012/428] fix(libc): save and restore the signal mask in setjmp and longjmp FreeBSD's setjmp saves the signal mask at 72 in the jump buffer and its longjmp restores it, unlike _setjmp and _longjmp. Ours kept only the registers, so a longjmp back from code that changed the mask, such as a signal handler, left the mask as it was. libc can't call libkernel, so libkernel registers its sigprocmask with libc when it loads. --- core/libs/prx/libc/include/JumpBuffer.hpp | 18 ++++++ core/libs/prx/libc/src/JumpBuffer.cpp | 59 +++++++++++++++++-- .../libs/prx/libkernel/System/src/Signals.cpp | 10 ++++ core/libs/tests/GuestSignalSets.cpp | 27 +++++++++ docs/dev/debt/libc.md | 1 + 5 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 core/libs/prx/libc/include/JumpBuffer.hpp diff --git a/core/libs/prx/libc/include/JumpBuffer.hpp b/core/libs/prx/libc/include/JumpBuffer.hpp new file mode 100644 index 0000000000..07e272b61e --- /dev/null +++ b/core/libs/prx/libc/include/JumpBuffer.hpp @@ -0,0 +1,18 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_JUMPBUFFER_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_JUMPBUFFER_HPP + +#include "prx/libc/include/general/VabiMacros.hpp" + +namespace JumpBuffer { + +using Sigprocmask = int (APS5_VABI*)(int how, const void* set, void* previousSet); + +extern "C" { + +void GuestSigprocmaskRegister_nid_no_patch(Sigprocmask sigprocmask); + +} + +} + +#endif diff --git a/core/libs/prx/libc/src/JumpBuffer.cpp b/core/libs/prx/libc/src/JumpBuffer.cpp index 77dca484e4..551a11c089 100644 --- a/core/libs/prx/libc/src/JumpBuffer.cpp +++ b/core/libs/prx/libc/src/JumpBuffer.cpp @@ -1,12 +1,62 @@ +#include "prx/libc/include/JumpBuffer.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/general/AsmMacros.hpp" +#include +#include +#include +#include + +namespace JumpBuffer { +namespace { + +constexpr std::size_t MaskOffset = 72; +constexpr std::size_t MaskSavedOffset = 88; +constexpr int SigBlock = 1; +constexpr int SigSetmask = 3; + +std::atomic guestSigprocmask{nullptr}; + +} + +extern "C" { + +void GuestSigprocmaskRegister_nid_no_patch(Sigprocmask sigprocmask) { + guestSigprocmask.store(sigprocmask); +} + +void APS5_VABI JumpBufferSaveMask_nid_no_patch(unsigned char* buffer) { + const auto sigprocmask = guestSigprocmask.load(); + const std::int32_t saved = sigprocmask != nullptr && sigprocmask(SigBlock, nullptr, buffer + MaskOffset) == 0; + std::memcpy(buffer + MaskSavedOffset, &saved, sizeof(saved)); +} + +void APS5_VABI JumpBufferRestoreMask_nid_no_patch(const unsigned char* buffer) { + std::int32_t saved = 0; + std::memcpy(&saved, buffer + MaskSavedOffset, sizeof(saved)); + const auto sigprocmask = guestSigprocmask.load(); + if (saved != 0 && sigprocmask != nullptr) sigprocmask(SigSetmask, buffer + MaskOffset, nullptr); +} + +} + +} // setjmp/longjmp must capture the guest's own frame, so they are written directly in assembly with // the guest (System V) calling convention. The saved state fits the guest's 96-byte jmp_buf: -// return address, rbx, rsp, rbp, r12-r15, MXCSR and the x87 control word. +// return address, rbx, rsp, rbp, r12-r15, MXCSR, the x87 control word and, as FreeBSD's do, the +// signal mask at 72 with the flag sigsetjmp keeps at 88, once libkernel has registered sigprocmask. + +#ifdef _WIN32 +#define APS5_JUMP_ASM_CALL(name) " call " name "\n" +#else +#define APS5_JUMP_ASM_CALL(name) " call " name "@PLT\n" +#endif asm(".text\n" APS5_ASM_FUNCTION("setjmp_nid_postfix") + " push %rdi\n" + APS5_JUMP_ASM_CALL("JumpBufferSaveMask_nid_no_patch") + " pop %rdi\n" " mov (%rsp), %rax\n" " mov %rax, 0(%rdi)\n" " mov %rbx, 8(%rdi)\n" @@ -22,15 +72,16 @@ asm(".text\n" " xor %eax, %eax\n" " ret\n" APS5_ASM_FUNCTION("longjmp_nid_postfix") -#ifdef _WIN32 " mov %rdi, %rbx\n" " mov %esi, %r12d\n" - " mov 16(%rdi), %rdi\n" " and $-16, %rsp\n" + APS5_JUMP_ASM_CALL("JumpBufferRestoreMask_nid_no_patch") +#ifdef _WIN32 + " mov 16(%rbx), %rdi\n" " call GuestStackSwitch_nid_no_patch\n" +#endif " mov %rbx, %rdi\n" " mov %r12d, %esi\n" -#endif " mov %esi, %eax\n" " test %eax, %eax\n" " jnz 1f\n" diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index b20877c19e..46d2427e43 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -1,5 +1,6 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/JumpBuffer.hpp" #include "prx/libkernel/Pthread/Posix/Common.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" #include "prx/libc/include/general/VabiMacros.hpp" @@ -630,3 +631,12 @@ int APS5_VABI _is_signal_return_nid_postfix(std::uint64_t programCounter) { } } + +namespace { + +[[maybe_unused]] const bool RegisteredForJumps = [] { + JumpBuffer::GuestSigprocmaskRegister_nid_no_patch(sigprocmask_nid_postfix); + return true; +}(); + +} diff --git a/core/libs/tests/GuestSignalSets.cpp b/core/libs/tests/GuestSignalSets.cpp index 798c736518..eee6e75af7 100644 --- a/core/libs/tests/GuestSignalSets.cpp +++ b/core/libs/tests/GuestSignalSets.cpp @@ -20,6 +20,8 @@ int* APS5_VABI __error_nid_postfix(); int APS5_VABI _sigprocmask_nid_postfix(int, const GuestSignalSet*, GuestSignalSet*); [[gnu::returns_twice]] int APS5_VABI sigsetjmp_nid_postfix(std::uint64_t* buffer, int saveMask); [[noreturn]] void APS5_VABI siglongjmp_nid_postfix(std::uint64_t* buffer, int value); +[[gnu::returns_twice]] int APS5_VABI setjmp_nid_postfix(std::uint64_t* buffer); +[[noreturn]] void APS5_VABI longjmp_nid_postfix(std::uint64_t* buffer, int value); int APS5_VABI JumpKeepingCalleeSaved(std::uint64_t* buffer); } @@ -164,9 +166,34 @@ static void CheckSigsetjmp() { SetBlockedMask(0); } +static int LongjumpBack(int value, std::uint32_t maskAtSet, std::uint32_t maskAtJump) { + std::uint64_t buffer[12]{}; + volatile int jumped = 0; + SetBlockedMask(maskAtSet); + const int result = setjmp_nid_postfix(buffer); + if (jumped == 0) { + Require(result == 0); + Require(BufferWord(buffer, 72) == maskAtSet); + Require(BufferWord(buffer, 88) == 1); + jumped = 1; + SetBlockedMask(maskAtJump); + longjmp_nid_postfix(buffer, value); + } + return result; +} + +static void CheckLongjmp() { + Require(LongjumpBack(0, 0x5u, 0x30u) == 1); + Require(BlockedMask() == 0x5u); + Require(LongjumpBack(7, 0x80000009u, 0u) == 7); + Require(BlockedMask() == 0x80000009u); + SetBlockedMask(0); +} + int main() { CheckSigsetjmp(); CheckSiglongjmp(); + CheckLongjmp(); static constexpr int Untouched = 5; GuestSignalSet set{{0x12345678u, 0x9abcdef0u, 0xffffffffu, 1u}}; *__error_nid_postfix() = Untouched; diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 124436d2ad..050e2a66bb 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -5,6 +5,7 @@ - [_Mtx_trylock](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `k6pGNMwJB08`) - returns busy for a non-recursive mutex already owned by the caller, assumed to match `_Mtx_lock`'s ownership handling; exact console behavior is unverified. - [nearbyintf](../../../core/libs/prx/libc/src/Math.cpp) (libc, NID `c+4r-T-tEIc`) uses the host rounding mode, not the console's; console rounding mode state remains unverified. - [_Thrd_sleep](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `jfRI3snge3o`) follows the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`xtime.c`) and C11 Annex K, without console verification. It reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep`, retrying on `EINTR`, so it is a cancellation point; a null xtime or one whose normalization overflows throws. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched. +- [setjmp, longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) save the signal mask at 72 in the jump buffer and restore it, as FreeBSD 11.4's [`setjmp` and `longjmp`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/amd64/gen/setjmp.S#L51-L85) do, which is unverified on a console. They go through the `sigprocmask` libkernel registers when it loads and mark the buffer at 88 as `sigsetjmp` does, so a buffer saved before then is jumped to without changing the mask - [_Mtx_init_with_name](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `tgioGpKtmbE`) forwards to `_Mtx_init` and ignores the mutex name parameter. - [_Cnd_timedwait](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `McaImWKXong`) follows the console's `_Cnd_timedwait` (disassembled from a title's `libc.prx`), where xtime is an absolute deadline converted against the current time: it returns `_Thrd_timedout` (2) on expiration and `_Thrd_success` (0) on notification. The console returns 4 for other internal pthread errors while this implementation throws. - [Guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws From 9f584de29b42f5a1ad557c157d5413461566f6a7 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:56:46 -0700 Subject: [PATCH 013/428] test(libkernel): leave a signal handler with longjmp and raise it again A handler that leaves with longjmp never returns to unblock its signal, so longjmp has to bring back the mask setjmp saved before the signal. --- core/libs/tests/GuestSignalHandlerMask.cpp | 23 ++++++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestSignalHandlerMask.cpp b/core/libs/tests/GuestSignalHandlerMask.cpp index 332bbbbb21..d118ab5fea 100644 --- a/core/libs/tests/GuestSignalHandlerMask.cpp +++ b/core/libs/tests/GuestSignalHandlerMask.cpp @@ -7,6 +7,8 @@ extern "C" { int APS5_VABI sigaction_nid_postfix(int, const void*, void*); int APS5_VABI sigprocmask_nid_postfix(int, const void*, void*); int APS5_VABI raise_nid_postfix(int); +[[gnu::returns_twice]] int APS5_VABI setjmp_nid_postfix(std::uint64_t* buffer); +[[noreturn]] void APS5_VABI longjmp_nid_postfix(std::uint64_t* buffer, int value); } struct GuestSignalSet { @@ -54,6 +56,13 @@ static void APS5_VABI Count(int) { ++calls; } +static std::uint64_t handlerExit[12]; + +static void APS5_VABI LeaveWithLongjmp(int) { + ++calls; + longjmp_nid_postfix(handlerExit, 1); +} + static void APS5_VABI BlockFromOtherThread(int) { ++calls; std::thread([] { @@ -110,11 +119,25 @@ static void CheckOtherThreadKept() { Require(CurrentMask() == 0); } +static void CheckLeftWithLongjmp() { + calls = 0; + Install(SigUsr1, LeaveWithLongjmp, SaRestart, Bit(SigUsr2)); + for (int round = 1; round <= 2; ++round) { + if (setjmp_nid_postfix(handlerExit) == 0) { + raise_nid_postfix(SigUsr1); + std::abort(); + } + Require(calls == round); + Require(CurrentMask() == 0); + } +} + int main() { CheckDeferred(SigUsr2); CheckDeferred(SigTerm); CheckNodefer(); CheckResethand(); CheckOtherThreadKept(); + CheckLeftWithLongjmp(); return 0; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 576d1c5a67..feec6807e1 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -153,7 +153,7 @@ - [sigtimedwait](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) waits on a process-wide pending set instead of throwing: a signal raised while blocked stays pending and is taken by `sigwait`, `sigwaitinfo` or `sigtimedwait`, delivered to the handler when `_sigprocmask` unblocks it, or delivered by `pause`. The set covers all 128 signals and the lowest number that is pending and in the set is selected (FreeBSD specifies the lowest number only among real-time signals). Selection, `EAGAIN` on timeout, `EINVAL` for a `tv_nsec` outside 0 to 1000000000 while nothing is pending and `NULL` timeout blocking indefinitely follow FreeBSD's [sigtimedwait(2)](https://man.freebsd.org/cgi/man.cgi?query=sigtimedwait). `sigwait` returns 0 with the number in its output, as POSIX specifies, and `sigwaitinfo` and `sigtimedwait` return the number, as FreeBSD does. The guest `siginfo_t` follows FreeBSD 11's `sys/sys/siginfo.h`: 80 bytes, `si_signo` at 0, `si_code` at 8, `si_pid` at 12, `si_uid` at 16, `si_addr` at 24, `si_value` at 32 and `si_band` at 40; only `si_signo`, `si_code` (`SI_USER`) and `si_pid` (the guest pid) are written, `si_uid` stays 0 because no uid is modelled, and the console's layout is not verified. PPSA85300 imports `sigtimedwait` and fails to load without it; its only call site is VLC's `vlc_writev` ([filesystem.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/posix/filesystem.c#L243-L278)), which polls with a zero timeout to drain a `SIGPIPE` only after `writev` fails with `EPIPE`. The waits are not cancellation points and an unblocked caught signal does not interrupt them with `EINTR`, where FreeBSD's `sigwaitinfo` and `sigtimedwait` may return it. `SIGPIPE` has no host equivalent and is never raised by a host write here (see `read, write and vector I/O`), so VLC's drain finds nothing - [pause, sigwait, sigwaitinfo](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) take the pending set of `sigtimedwait` instead of throwing. `pause` waits for a pending signal that is not blocked and whose disposition can be delivered, runs its handler on the calling thread and returns -1 with `EINTR`; an ignored signal is discarded and the wait continues, and it never returns for a signal whose handler cannot run. A signal whose registered handler has `SA_SIGINFO` still throws, and one whose disposition is the default action and that has no host equivalent (every signal except 2, 4, 6, 8, 11 and 15) is neither delivered nor discarded: it stays pending until a wait takes it, where FreeBSD runs the default action, which terminates the process for most of them. PPSA68091, PPSA99169, PPSA99420 and PPSA97358 import them and fail to load without them. PPSA68091 calls `pause` at one site (0x8F7266), in a loop that waits while a global holds another thread's `errno` address. PPSA99169 only stores the address of `pause` in a table of symbol addresses (0x30DB1). PPSA99420 calls `pause` at 0x3BD80F6 and 0x3C9E220, `sigwait` at 0x3C9E4C7 and `sigwaitinfo` at 0x3C9E588. PPSA97358 calls `sigwait` from QEMU's `sigwait_compat` thread ([compatfd.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/compatfd.c#L28-L53)), which `qemu_signal_init` ([main-loop.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/main-loop.c#L110-L147)) starts for `SIGIO`, `SIGALRM` and `SIGBUS` when the main loop is created, so it reaches the wait at start-up, and from the vCPU thread of the dummy accelerator ([dummy-cpus.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/accel/dummy-cpus.c#L48-L56)). None of the four waits is a cancellation point - [getsid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns the calling process ID for pid 0 and the calling process, and `ESRCH` for other process IDs, assuming the single guest process is its own session leader. PPSA74126 imports it and fails to load without it -- [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do. While a handler runs, its own signal (unless `SA_NODEFER`) and the first 32 signals of its mask are added to the process-wide mask, and `SA_RESETHAND` resets the disposition to the default before it runs, as FreeBSD's `postsig_done` does. When the handler returns, only the signals it added are unblocked again, so a mask change another thread made meanwhile is kept, but so is one the handler made itself, which FreeBSD's `sigreturn` would undo; a signal raised meanwhile stays pending and is delivered when the handler returns to `raise`, but one left pending by a handler that a host signal ran asynchronously waits for the next `raise` or `_sigprocmask`. `SA_ONSTACK` is not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating +- [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do. While a handler runs, its own signal (unless `SA_NODEFER`) and the first 32 signals of its mask are added to the process-wide mask, and `SA_RESETHAND` resets the disposition to the default before it runs, as FreeBSD's `postsig_done` does. When the handler returns, only the signals it added are unblocked again, so a mask change another thread made meanwhile is kept, but so is one the handler made itself, which FreeBSD's `sigreturn` would undo. A handler left with `longjmp`, or with `siglongjmp` to a buffer that saved the mask, gets back the mask that buffer saved, as on FreeBSD; a signal raised meanwhile stays pending and is delivered when the handler returns to `raise`, but one left pending by a handler that a host signal ran asynchronously waits for the next `raise` or `_sigprocmask`. `SA_ONSTACK` is not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating - [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console - [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `EINVAL` for every other command, `F_GETFD` and `F_SETFD` included). The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux, where `F_GETLK` with `F_UNLCK` throws. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console From c318c70b69105bf36c7bdbe1ea471a48f2d0a4ea Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:54:54 +0200 Subject: [PATCH 014/428] test(libSceVideoOut): run the telemetry tests on Windows video_out_telemetry and video_out_telemetry_caption were registered only for non-Windows hosts. Telemetry.cpp needs getpid, which Windows spells _getpid, and a narrow path for OpenFromEnvironment, because a std::filesystem::path c_str() is wide there. Register the tests with the Windows PATH and unwind settings of the other video out tests; the caption test skips (77) when the offscreen SDL driver has no Vulkan support. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceVideoOut/CMakeLists.txt | 7 ++++++- core/libs/prx/libSceVideoOut/tests/Telemetry.cpp | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 9c7f9ae6da..da5a8f0b33 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -274,13 +274,18 @@ if(BUILD_TESTING) endif() endif() -if(BUILD_TESTING AND NOT WIN32) +if(BUILD_TESTING) add_test_executable(video_out_telemetry_tests tests/Telemetry.cpp) target_include_directories(video_out_telemetry_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) target_link_libraries(video_out_telemetry_tests PRIVATE libSceVideoOut libSceAgcDriver libkernel libc SDL2-static) set_target_properties(video_out_telemetry_tests PROPERTIES CXX_EXTENSIONS OFF) + configure_windows_unwind(video_out_telemetry_tests) add_test(NAME video_out_telemetry COMMAND video_out_telemetry_tests) add_test(NAME video_out_telemetry_caption COMMAND video_out_telemetry_tests caption) set_tests_properties(video_out_telemetry PROPERTIES TIMEOUT 30) set_tests_properties(video_out_telemetry_caption PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0 ENVIRONMENT "SDL_VIDEODRIVER=${ANYPS5_VIDEO_OUT_TEST_DRIVER}") + if(WIN32) + set_tests_properties(video_out_telemetry video_out_telemetry_caption PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceVideoOut/tests/Telemetry.cpp b/core/libs/prx/libSceVideoOut/tests/Telemetry.cpp index d3fc3ce183..ba808eb7ba 100644 --- a/core/libs/prx/libSceVideoOut/tests/Telemetry.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Telemetry.cpp @@ -3,7 +3,12 @@ #include #include +#ifdef _WIN32 +#include +#define getpid _getpid +#else #include +#endif #include #include #include @@ -109,7 +114,7 @@ void queueOverflowIsCounted() { void contractPadAndPresentLines() { const auto directory = freshDirectory("contract"); { - auto writer = VideoOutTelemetry::OpenFromEnvironment(directory.c_str(), "abc123"); + auto writer = VideoOutTelemetry::OpenFromEnvironment(directory.string().c_str(), "abc123"); check(writer != nullptr, "variable must open a writer"); PadInputState state; state.buttons = 0x4000; From 1d3dc17184309b08e934d5654244b5e12c678032 Mon Sep 17 00:00:00 2001 From: Baytowne <30433191+Baytowne@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:17:31 -0500 Subject: [PATCH 015/428] fix(tools): report an unknown ref as a usage error in progress_history --- tools/progress_history.py | 7 ++++++- tools/tests/test_progress_history.py | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/tools/progress_history.py b/tools/progress_history.py index ca8460652a..25d5868451 100644 --- a/tools/progress_history.py +++ b/tools/progress_history.py @@ -25,8 +25,13 @@ def git(*args): def daily_commits(ref): + try: + log = git("log", "--first-parent", "--format=%H %ct", ref) + except subprocess.CalledProcessError as error: + reason = error.stderr.decode("utf-8", errors="replace").strip() + raise ValueError(f"cannot read the history of {ref}: {reason}") from error seen = {} - for line in git("log", "--first-parent", "--format=%H %ct", ref).decode().splitlines(): + for line in log.decode().splitlines(): sha, timestamp = line.split() seen.setdefault(datetime.fromtimestamp(int(timestamp), timezone.utc).date().isoformat(), sha) return sorted(seen.items()) diff --git a/tools/tests/test_progress_history.py b/tools/tests/test_progress_history.py index 813b596990..7cc7643c54 100644 --- a/tools/tests/test_progress_history.py +++ b/tools/tests/test_progress_history.py @@ -98,6 +98,17 @@ class ProgressHistoryTests(unittest.TestCase): self.assertIn(" Date: Tue, 20 Oct 2026 18:10:13 +0100 Subject: [PATCH 016/428] fix(libkernel): notify a POSIX semaphore waiter before sem_post releases its lock sem_post raised the count under the semaphore's lock, released the lock, and only then called NotifyOne on the heap PosixSemaphore. A thread that took the count in that gap could call sem_destroy, which deletes the object, and the poster then ran NotifyOne on freed memory. FreeBSD 11.4's sem_post touches only the caller's sem_t after it publishes the token, and its sem_destroy only clears _magic, so a thread may destroy the semaphore as soon as its wait returns. Call NotifyOne before the lock is released. A thread takes the count only under that lock, so the unlock is the poster's last access. guest_posix_sem_post now runs 20000 rounds that post to a semaphore the main thread destroys once sem_trywait gets the count; with the old order the poster crashes in NotifyOne. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Pthread/Posix/Sem.cpp | 8 +++---- core/libs/tests/GuestPosixSem.cpp | 24 +++++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp b/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp index e780d7f59f..4cdb924c36 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp @@ -108,11 +108,9 @@ int APS5_VABI sem_reltimedwait_np_nid_postfix(void* sem, uint32_t usec) { int APS5_VABI sem_post_nid_postfix(void* sem) { auto* semaphore = Get(sem); if (!semaphore) return Fail(PosixThread::GUEST_EINVAL); - { - std::lock_guard lock(semaphore->lock); - if (semaphore->count == GUEST_SEM_VALUE_MAX) return Fail(GUEST_EOVERFLOW); - ++semaphore->count; - } + std::lock_guard lock(semaphore->lock); + if (semaphore->count == GUEST_SEM_VALUE_MAX) return Fail(GUEST_EOVERFLOW); + ++semaphore->count; semaphore->available.NotifyOne(); return 0; } diff --git a/core/libs/tests/GuestPosixSem.cpp b/core/libs/tests/GuestPosixSem.cpp index f6a2963120..1fa24b8cf9 100644 --- a/core/libs/tests/GuestPosixSem.cpp +++ b/core/libs/tests/GuestPosixSem.cpp @@ -26,6 +26,29 @@ static void Require(bool condition, const char* message) { } } +static void CheckRetire() { + constexpr int rounds = 20000; + std::atomic published{nullptr}; + std::thread poster([&] { + for (int round = 0; round < rounds; ++round) { + published.wait(nullptr, std::memory_order_acquire); + std::uintptr_t* target = published.exchange(nullptr, std::memory_order_acq_rel); + Require(sem_post_nid_postfix(target) == 0, "post to a semaphore its waiter destroys must succeed"); + } + }); + for (int round = 0; round < rounds; ++round) { + std::uintptr_t slot = 0; + Require(sem_init_nid_postfix(&slot, 0, 0) == 0, "retired semaphore must initialize"); + published.store(&slot, std::memory_order_release); + published.notify_one(); + for (int spins = 0; sem_trywait_nid_postfix(&slot) != 0; ++spins) { + if (spins >= 4096) std::this_thread::yield(); + } + Require(sem_destroy_nid_postfix(&slot) == 0, "semaphore must be destroyable once its count is taken"); + } + poster.join(); +} + int main(int argc, char** argv) { constexpr unsigned int maximum = 0x7fffffffu; std::uintptr_t sem = 0; @@ -99,4 +122,5 @@ int main(int argc, char** argv) { Require(sem_timedwait_nid_postfix(&sem, &expiredTimeout) == -1 && *__error_nid_postfix() == 60, "blocked expired-timeout wait must report ETIMEDOUT"); Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "failed timed waits must preserve the count"); Require(sem_destroy_nid_postfix(&sem) == 0, "blocked semaphore destroy must succeed"); + CheckRetire(); } From ab6a4f9e16df32b599d657ea5ec176fa1f80eb93 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Wed, 21 Oct 2026 01:54:05 +0300 Subject: [PATCH 017/428] perf(agc): scale default registration prepare pool threads with hardware concurrency --- .../Execution/src/Driver/Shaders/ShaderRegistry.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index b69bccd8fe..a478375483 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1124,7 +1124,7 @@ public: static auto* pool = [] { const char* text = std::getenv("APS5_PREBUILD_THREADS"); std::lock_guard lifecycle(Lifecycle()); - auto* created = new RegistrationPreparePool(Closing() ? 0u : text != nullptr ? std::max(1u, static_cast(std::strtoul(text, nullptr, 10))) : 1u); + auto* created = new RegistrationPreparePool(Closing() ? 0u : text != nullptr ? std::max(1u, static_cast(std::strtoul(text, nullptr, 10))) : std::max(2u, std::thread::hardware_concurrency() / 2u)); PrebuildSlot().store(created); return created; }(); From 0a281e83073ee5b4568b4ed32b163353f094355b Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Wed, 21 Oct 2026 01:54:13 +0300 Subject: [PATCH 018/428] perf(agc): skip duplicate partialThreads compute variant at shader registration --- .../Execution/src/Driver/Shaders/ShaderRegistry.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index a478375483..badf48b3c0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1051,10 +1051,7 @@ std::vector PrepareRegistered(RegisteredPreparation& pla APS5_LOG_OUT("shader library prebuild skipped: %s", error.what()); } } - if (request.context.compute) { - request.context.compute->partialThreads = {1, 1, 1}; - append(); - } else if (request.context.pixel) { + if (request.context.pixel) { request.layout.pushConstantSizeBytes = 0; append(); } From dd3a510bb2ce9a5e4e78fe2968d303a1a6e93125 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Wed, 21 Oct 2026 01:54:20 +0300 Subject: [PATCH 019/428] perf(agc): add VulkanDevice::PrebuildComputePipeline and prebuild resolved dispatches --- .../Execution/include/VulkanDevice.hpp | 1 + .../Execution/src/Driver/Dispatch/Dispatch.cpp | 8 ++++++++ .../Execution/src/VulkanDevice.cpp | 17 +++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 19b1b24e22..558cf49b4a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -42,6 +42,7 @@ public: // Builds the pipeline of a journaled compute result ahead of its first dispatch (the journal // replay's hook, on its threads): false when the device already holds a pipeline for the module. bool ReplayComputePipeline(const ShaderRecompiler::RecompileResult& shader, bool pushConstants); + void PrebuildComputePipeline(const ShaderRecompiler::RecompileResult& shader, std::uint64_t programAddress = 0); // Starts the journal replay if the device has not (a headless device starts it at its first // draw or dispatch) and joins it (tests; the teardown joins it anyway). void WaitForPipelineJournalReplay(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp index b840a0df06..b49f89d46b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -399,6 +399,14 @@ void Driver::dispatch(QueueState& queue, std::span packet, parked.resolvedAt = std::chrono::steady_clock::now(); ++groupStats.resolved; if (cached) ++groupStats.resolvedHits; + if (compiledResult != nullptr) { + SubmitLibraryPrebuild([localDevice, compiledResult, address] { + try { + localDevice->PrebuildComputePipeline(*compiledResult, address); + } catch (...) { + } + }); + } return; } if (verifyDataHits() && dataHit) verifyDataHit(snapshot, codeOffset, request, memory, address, *keepVariant, liveWords, *compiledResult); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index e916f573d4..e987aaa3a4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2135,6 +2135,23 @@ bool VulkanDevice::ReplayComputePipeline(const ShaderRecompiler::RecompileResult return state->computePipelines.try_emplace(key, std::move(objects)).second; } +void VulkanDevice::PrebuildComputePipeline(const ShaderRecompiler::RecompileResult& shader, std::uint64_t programAddress) { + if (shader.spirv.size() < 5 || shader.spirv[0] != 0x07230203u) return; + ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion); + const auto context = graphicsContext(); + if (context.descriptorCache == nullptr) return; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Compute, &shader, 0}}}; + const auto pushStages = Graphics::PushConstantStages(shaders); + if (pushStages != 0 && state->properties.limits.maxPushConstantsSize < Graphics::ComputePushConstantBytes) return; + const auto key = ComputePipelineKey::Of(shader, pushStages != 0); + if (!key.identity.Empty()) { + std::lock_guard pipelines(state->computePipelinesMutex); + if (state->computePipelines.contains(key) || state->computePipelinesBuilding.contains(key)) return; + } + const auto layouts = Graphics::DescribeSetLayouts(context, shaders); + static_cast(computePipeline(context, shader, layouts.layouts.main, layouts.mainKey, pushStages, programAddress)); +} + bool VulkanDevice::ImportGuestMemory(const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice) { if (adoptDevice) GuestMemory::AssertGpuLockHeld("VulkanDevice::ImportGuestMemory"); return Graphics::ImportMappedRanges(graphicsContext(), ranges, generation, adoptDevice); From 2347678b40592dc8d685089e0fb6b498dd94eff1 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Wed, 21 Oct 2026 01:54:27 +0300 Subject: [PATCH 020/428] perf(agc): prewarm compute shader invocations during ahead-of-time group resolution --- .../src/Driver/Dispatch/DispatchAhead.cpp | 100 +++++++++++++++++- 1 file changed, 98 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp index 0b6f1f0eb0..56c67fa3eb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp @@ -1,9 +1,11 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "ThreadOwned.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" +#include "Optimization/ResourceProgram.hpp" #include #include #include @@ -103,10 +105,14 @@ bool OverlapsLabel(std::span regions, cons } void Driver::resolveGroupAhead(const Submission& submission, const QueueState& live, std::size_t from) { - static const bool allQueues = std::getenv("APS5_GROUP_CAPTURE_ALL_QUEUES") != nullptr; + static const bool allQueues = [] { + const char* text = std::getenv("APS5_GROUP_CAPTURE_ALL_QUEUES"); + return text == nullptr || (text[0] != '0' && text[0] != '\0'); + }(); if (!allQueues && submission.queue == 0) return; auto& resolved = resolvedAhead(); - if (resolved.contains(from) || device.Load() == nullptr) return; + const auto localDevice = device.Load(); + if (resolved.contains(from) || localDevice == nullptr) return; auto& stats = groupCaptureStats(); const auto begin = std::chrono::steady_clock::now(); ++stats.runs; @@ -124,6 +130,88 @@ void Driver::resolveGroupAhead(const Submission& submission, const QueueState& l } } resolving; const auto& commands = submission.commands; + if (submission.shaders != nullptr) { + QueueState preview = live; + std::vector pendingShaders; + std::vector submittedShaders; + for (std::size_t scan = from; scan < commands.size();) { + const auto header = commands[scan]; + if (Pm4::FillerPacket(header)) { + ++scan; + continue; + } + const auto count = Pm4::PacketWords(header); + if (scan + count > commands.size()) break; + const auto packet = std::span(commands).subspan(scan, count); + const auto opcode = (header >> 8u) & 0xffu; + const auto next = scan + count; + if (header == FlipPacketHeader || opcode == 0x22 || opcode == 0x3f) break; + if (opcode == 0x15 || opcode == 0x16) { + try { + const auto low = preview.shader.find(0x20c); + const auto high = preview.shader.find(0x20d); + if (low != preview.shader.end() && high != preview.shader.end()) { + const auto address = (static_cast(low->second) << 8u) | (static_cast(high->second & 0xffu) << 40u); + if (auto registered = RegisteredProgram(*submission.shaders, address, {0}); registered != nullptr && registered->type == 0) { + bool isPending = false; + bool isEmpty = false; + { + std::lock_guard lock(registered->prepared->mutex); + isPending = registered->prepared->pending; + isEmpty = registered->prepared->entries.empty(); + } + if (isPending) { + if (std::ranges::find(pendingShaders, registered->prepared.get()) == pendingShaders.end()) { + pendingShaders.push_back(registered->prepared.get()); + } + } else if (isEmpty && std::ranges::find(submittedShaders, registered->prepared.get()) == submittedShaders.end()) { + submittedShaders.push_back(registered->prepared.get()); + const auto rsrc2 = preview.shader.find(0x213); + const auto userCount = rsrc2 != preview.shader.end() ? ((rsrc2->second >> 1u) & 0x1fu) : 0u; + std::vector userData(userCount, 0u); + auto compute = Graphics::DecodeComputeStageInfo(preview.shader, registered->header); + const auto wave = (packet[4] & 0x8000u) != 0 ? 32u : 64u; + const auto codeOffset = static_cast((address - registered->codeAddress) / sizeof(std::uint32_t)); + SubmitRegistrationPreparation([registered, localDevice, address, codeOffset, wave, compute, userData = std::move(userData), state = preview.shader]() mutable { + try { + std::vector memory{{registered->codeAddress, std::as_bytes(std::span(registered->code))}}; + if (!registered->header.empty()) memory.push_back({registered->headerAddress, registered->header}); + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::ShaderStage::Compute, address, std::span(registered->code).subspan(codeOffset), registered->headerAddress, registered->header}, + {wave, 0, userData, compute, std::nullopt, std::nullopt, memory, DispatchFloatMode(*registered, state)}, + localDevice->ComputeTarget(wave), + {0, 0, 0, 128} + }; + static_cast(InvocationFor(*registered, codeOffset, request)); + } catch (...) { + } + }, registered->prepared.get()); + } + } + } + Pm4::NoteDispatchRegisters(packet, preview); + } catch (...) { + } + } else if (Pm4::IndirectRegisterOpcode(opcode)) { + const auto found = submission.registerLists.find(scan); + if (found != submission.registerLists.end() && Pm4::IndirectRegistersValid(found->second)) { + try { + Pm4::ExecuteIndirectRegisters(packet, found->second, preview); + } catch (...) { + } + } + } else if (!Pm4::AccessesMemory(header) && opcode != 0x3c && opcode != 0x93 && opcode != 0x37 && opcode != 0x42 && opcode != 0x46 && opcode != 0x49 && opcode != 0x58) { + try { + Pm4::Execute(packet, preview); + } catch (...) { + } + } + scan = next; + } + for (auto it = pendingShaders.rbegin(); it != pendingShaders.rend(); ++it) { + PrioritizeRegistrationPreparation(**it); + } + } for (std::size_t cursor = from; cursor < commands.size();) { const auto header = commands[cursor]; if (Pm4::FillerPacket(header)) { @@ -185,6 +273,14 @@ void Driver::resolveGroupAhead(const Submission& submission, const QueueState& l if (!label) break; NoteLabel(labels, label->address, label->Bytes()); writes.emplace_back(label->address, label->address + label->Bytes().size()); + } else if (Pm4::IndirectRegisterOpcode(opcode)) { + const auto found = submission.registerLists.find(cursor); + if (found == submission.registerLists.end() || !Pm4::IndirectRegistersValid(found->second)) break; + try { + Pm4::ExecuteIndirectRegisters(packet, found->second, scratch); + } catch (const std::exception&) { + break; + } } else if (opcode != 0x3f && opcode != 0x42 && opcode != 0x46 && opcode != 0x58) { if (Pm4::AccessesMemory(header)) break; try { From 6aab4eded105066cdacb5a0c068e918d5bd39d1e Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Wed, 21 Oct 2026 01:54:34 +0300 Subject: [PATCH 021/428] perf(agc): trigger group-ahead resolution on uncaptured dispatches across queues --- .../Execution/src/Driver/Packets/PacketExecution.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index 4a4e4e457f..5a5a3e1ef8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -226,6 +226,7 @@ void Driver::execute(const Submission& submission) { struct ClearAhead { ~ClearAhead() { clearResolvedAhead(); } } clearAhead; + std::size_t aheadCursor = 0; for (std::size_t cursor = 0; cursor < submission.commands.size();) { if (packetEpoch()) bumpEpoch(&EpochBumps::packets); CheckFailure(); @@ -390,9 +391,10 @@ void Driver::execute(const Submission& submission) { packetCounters.CountDispatch(); landPendingWaits(submission.queue); currentPacketOffset() = cursor; - if (gateOpened()) { + if (gateOpened() || (resolvedAhead().empty() && cursor >= aheadCursor)) { gateOpened() = false; - if (!pipelineDispatch() || submission.queue != 0) { + aheadCursor = nextCursor; + if (!pipelineDispatch() || opcode == 0x16 || !DrawPipeline::Active() || submission.queue != 0) { if (pipelined && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, opcode); timed(&WorkerProfile::dispatchMs, [&] { resolveGroupAhead(submission, queue, cursor); }); } From 8685c2edece328b2a14686b556b3eb0655d4b2a8 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Wed, 21 Oct 2026 04:17:31 +0300 Subject: [PATCH 022/428] fix(agc): gate dispatch look-ahead packet length and read initiator from packet.back() --- .../src/Driver/Dispatch/Dispatch.cpp | 5 +- .../src/Driver/Dispatch/DispatchAhead.cpp | 49 ++++++--------- .../tests/execution/PreparedShaders.cpp | 63 ++++++++++++++++++- 3 files changed, 82 insertions(+), 35 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp index b49f89d46b..06eb91faee 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -401,10 +401,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, if (cached) ++groupStats.resolvedHits; if (compiledResult != nullptr) { SubmitLibraryPrebuild([localDevice, compiledResult, address] { - try { - localDevice->PrebuildComputePipeline(*compiledResult, address); - } catch (...) { - } + try { localDevice->PrebuildComputePipeline(*compiledResult, address); } catch (...) {} }); } return; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp index 56c67fa3eb..e32182b7cb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp @@ -147,6 +147,7 @@ void Driver::resolveGroupAhead(const Submission& submission, const QueueState& l const auto next = scan + count; if (header == FlipPacketHeader || opcode == 0x22 || opcode == 0x3f) break; if (opcode == 0x15 || opcode == 0x16) { + if (count < (opcode == 0x15 ? 5u : 3u)) break; try { const auto low = preview.shader.find(0x20c); const auto high = preview.shader.find(0x20d); @@ -165,15 +166,18 @@ void Driver::resolveGroupAhead(const Submission& submission, const QueueState& l pendingShaders.push_back(registered->prepared.get()); } } else if (isEmpty && std::ranges::find(submittedShaders, registered->prepared.get()) == submittedShaders.end()) { - submittedShaders.push_back(registered->prepared.get()); - const auto rsrc2 = preview.shader.find(0x213); - const auto userCount = rsrc2 != preview.shader.end() ? ((rsrc2->second >> 1u) & 0x1fu) : 0u; - std::vector userData(userCount, 0u); - auto compute = Graphics::DecodeComputeStageInfo(preview.shader, registered->header); - const auto wave = (packet[4] & 0x8000u) != 0 ? 32u : 64u; const auto codeOffset = static_cast((address - registered->codeAddress) / sizeof(std::uint32_t)); - SubmitRegistrationPreparation([registered, localDevice, address, codeOffset, wave, compute, userData = std::move(userData), state = preview.shader]() mutable { - try { + if (codeOffset < registered->code.size()) { + submittedShaders.push_back(registered->prepared.get()); + const auto rsrc2 = preview.shader.find(0x213); + const auto userCount = rsrc2 != preview.shader.end() ? ((rsrc2->second >> 1u) & 0x1fu) : 0u; + std::vector userData(userCount, 0u); + for (std::uint32_t i = 0; i < userCount; ++i) { + if (const auto reg = preview.shader.find(0x240u + i); reg != preview.shader.end()) userData[i] = reg->second; + } + auto compute = Graphics::DecodeComputeStageInfo(preview.shader, registered->header); + const auto wave = (packet.back() & 0x8000u) != 0 ? 32u : 64u; + SubmitRegistrationPreparation([registered, localDevice, address, codeOffset, wave, compute, userData = std::move(userData), state = preview.shader]() mutable { std::vector memory{{registered->codeAddress, std::as_bytes(std::span(registered->code))}}; if (!registered->header.empty()) memory.push_back({registered->headerAddress, registered->header}); ShaderRecompiler::RecompileRequest request{ @@ -182,29 +186,20 @@ void Driver::resolveGroupAhead(const Submission& submission, const QueueState& l localDevice->ComputeTarget(wave), {0, 0, 0, 128} }; - static_cast(InvocationFor(*registered, codeOffset, request)); - } catch (...) { - } - }, registered->prepared.get()); + try { static_cast(InvocationFor(*registered, codeOffset, request)); } catch (...) {} + }, registered->prepared.get()); + } } } } - Pm4::NoteDispatchRegisters(packet, preview); - } catch (...) { - } + Pm4::NoteDispatchRegisters(packet, preview); } catch (...) {} } else if (Pm4::IndirectRegisterOpcode(opcode)) { const auto found = submission.registerLists.find(scan); if (found != submission.registerLists.end() && Pm4::IndirectRegistersValid(found->second)) { - try { - Pm4::ExecuteIndirectRegisters(packet, found->second, preview); - } catch (...) { - } + try { Pm4::ExecuteIndirectRegisters(packet, found->second, preview); } catch (...) {} } } else if (!Pm4::AccessesMemory(header) && opcode != 0x3c && opcode != 0x93 && opcode != 0x37 && opcode != 0x42 && opcode != 0x46 && opcode != 0x49 && opcode != 0x58) { - try { - Pm4::Execute(packet, preview); - } catch (...) { - } + try { Pm4::Execute(packet, preview); } catch (...) {} } scan = next; } @@ -227,7 +222,7 @@ void Driver::resolveGroupAhead(const Submission& submission, const QueueState& l if (opcode == 0x3c || opcode == 0x93) { if (!Pm4::WaitSatisfiedUnchecked(packet) && !WrittenByRun(packet, labels)) break; } else if (opcode == 0x15 || opcode == 0x16) { - if (resolved.contains(cursor)) break; + if (count < (opcode == 0x15 ? 5u : 3u) || resolved.contains(cursor)) break; if (opcode == 0x16) { std::uint64_t arguments = 0; try { @@ -276,11 +271,7 @@ void Driver::resolveGroupAhead(const Submission& submission, const QueueState& l } else if (Pm4::IndirectRegisterOpcode(opcode)) { const auto found = submission.registerLists.find(cursor); if (found == submission.registerLists.end() || !Pm4::IndirectRegistersValid(found->second)) break; - try { - Pm4::ExecuteIndirectRegisters(packet, found->second, scratch); - } catch (const std::exception&) { - break; - } + try { Pm4::ExecuteIndirectRegisters(packet, found->second, scratch); } catch (const std::exception&) { break; } } else if (opcode != 0x3f && opcode != 0x42 && opcode != 0x46 && opcode != 0x58) { if (Pm4::AccessesMemory(header)) break; try { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index 734a7c6825..00c0066a6f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -1261,6 +1261,64 @@ void RelocatedHeaders() { ExpectFailure([&] { AgcDriverResolveShaderAbi_nid_postfix(&otherContext.shader, {}, primitive); }, "replaced shader header"); } +void DispatchAheadPrewarm() { + alignas(256) std::array pendingCode{0xbf810000u}; + alignas(256) std::array lazyCode{0xbf810000u}; + struct Header { + Shader shader{}; + std::array registers{}; + ShaderSpecialRegs specials{}; + } pendingHeader, lazyHeader; + const auto fillHeader = [](Header& header, const std::array& code) { + const auto address = reinterpret_cast(code.data()); + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = sizeof(code); + header.shader.code = code.data(); + header.shader.sh_registers = header.registers.data(); + header.shader.num_sh_registers = header.registers.size(); + header.shader.specials = &header.specials; + header.specials.dispatch_modifier = 0x8000; + header.registers = {{{0x20c, static_cast(address >> 8u)}, {0x20d, static_cast(address >> 40u)}, {0x207, 1}, {0x208, 1}, {0x209, 1}, {0x212, 0}, {0x213, 2}}}; + }; + fillHeader(pendingHeader, pendingCode); + fillHeader(lazyHeader, lazyCode); + AgcDriverRegisterShader_nid_postfix(&pendingHeader.shader); + AgcDriverRegisterShader_nid_postfix(&lazyHeader.shader); + AgcDriver::DriverDetail::AwaitRegistrationPreparation(); + const auto registry = AgcDriver::DriverDetail::Driver::Get().RegisteredShaders(); + Require(registry != nullptr, "dispatch ahead prewarm test published no shader registry"); + const auto pendingSnapshot = AgcDriver::DriverDetail::RegisteredHeader(*registry, &pendingHeader.shader); + const auto lazySnapshot = AgcDriver::DriverDetail::RegisteredHeader(*registry, &lazyHeader.shader); + Require(pendingSnapshot != nullptr && lazySnapshot != nullptr, "dispatch ahead prewarm test published no snapshots"); + std::vector savedEntries; + { + std::lock_guard lock(pendingSnapshot->prepared->mutex); + savedEntries = std::move(pendingSnapshot->prepared->entries); + pendingSnapshot->prepared->entries.clear(); + pendingSnapshot->prepared->pending = true; + } + { + std::lock_guard lock(lazySnapshot->prepared->mutex); + lazySnapshot->prepared->entries.clear(); + lazySnapshot->prepared->pending = false; + } + auto settle = std::async(std::launch::async, SettleLater, std::ref(*pendingSnapshot->prepared), std::move(savedEntries), nullptr); + alignas(16) const std::array indirectPairs{0x20cu, lazyHeader.registers[0].value, 0x20du, lazyHeader.registers[1].value, 0x240u, 7u}; + const auto pairsAddress = reinterpret_cast(indirectPairs.data()); + std::vector commands; + for (const auto reg : pendingHeader.registers) commands.insert(commands.end(), {0xc0017600u, reg.offset, reg.value}); + commands.insert(commands.end(), {0xc0031500u, 1, 1, 1, 0x8041u, 0xffff1000u}); + commands.insert(commands.end(), {0xc0036300u, static_cast(pairsAddress), static_cast(pairsAddress >> 32u), 0x80000000u, 3u}); + commands.insert(commands.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(0x20, &packet) == 0, "dispatch ahead prewarm submit failed"); + AgcDriverWaitIdle_nid_postfix(); + settle.get(); + AgcDriverShutdown_nid_postfix(); +} + } int main(int argc, char** argv) { @@ -1288,10 +1346,11 @@ int main(int argc, char** argv) { return 0; } if (argc == 2 && std::string_view(argv[1]) == "--registered-workgroup") { - const auto device = OpenVulkanTestDevice(); + auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; RegisteredVertexWorkgroup(device->Target(), true); - AgcDriverShutdown_nid_postfix(); + device.reset(); + DispatchAheadPrewarm(); return 0; } if (argc == 2 && std::string_view(argv[1]) == "--deferred") { From 994f69db46fef94113f82f485b63dc2eea331e60 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Wed, 21 Oct 2026 11:40:09 +0700 Subject: [PATCH 023/428] fix(shader): apply v_dot2_f32_f16's accumulator modifiers as RDNA 2 does The decoder refused v_dot2_f32_f16 whenever op_sel[2] or neg_hi[2] was set or op_sel_hi[2] was clear. On an RX 6700 XT the f32 accumulator ignores op_sel[2] and op_sel_hi[2], neg_lo[2] negates it and neg_hi[2] takes its absolute value (both together give -|c|), as for v_fma_mix. The decoder now normalizes the accumulator's selects and turns neg_hi[2] into abs. Float16Dot's refusal checks become run checks of the five accumulator variants against values measured on the same rows. Co-Authored-By: Claude --- .../tests/execution/Float16Dot.cpp | 71 ++++++++++++++----- .../RdnaDecoder/src/RdnaVectorOpDecoder.cpp | 9 ++- 2 files changed, 60 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Float16Dot.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Float16Dot.cpp index 961afeb7c9..0a96011da8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Float16Dot.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Float16Dot.cpp @@ -31,6 +31,12 @@ alignas(256) constexpr std::array Code{ 0xe0781020, 0x80011203, 0xe0781030, 0x80011603, 0xbf810000, }; +alignas(256) constexpr std::array AccumulatorCode{ + 0x34020084, 0x34060086, 0xe0381000, 0x80000401, 0xbf8c3f70, 0xcc13600a, 0x1c1a0b04, 0xcc13000b, + 0x1c1a0b04, 0xcc13440c, 0x1c1a0b04, 0xcc13400d, 0x9c1a0b04, 0xcc13440e, 0x9c1a0b04, 0xe0781000, + 0x80010a03, 0xe0701010, 0x80010e03, 0xbf810000, +}; + constexpr std::uint32_t Rows[32][4] = { {0x0001bc00u, 0x81bfacbau, 0xc541aa2fu, 0x00000000u}, {0x01f61a79u, 0x3c000381u, 0xc1729fffu, 0x00000000u}, @@ -101,6 +107,43 @@ constexpr std::uint32_t Expected[32][10] = { {0xbaf43c38u, 0xbaf43c38u, 0x3af43c38u, 0x3af43c38u, 0x3abe63ecu, 0xbaf43c38u, 0xc021e000u, 0x3fb50000u, 0xba90cd60u, 0x3abe63ecu} }; +constexpr std::uint32_t AccumulatorExpected[32][5] = { + {0xc541a901u, 0xc541a901u, 0x4541ab5du, 0x4541ab5du, 0xc541a901u}, + {0xc1729fdfu, 0xc1729fdfu, 0x4172a01fu, 0x4172a01fu, 0xc1729fdfu}, + {0x4187e315u, 0x4187e315u, 0x4187e315u, 0xc1882737u, 0xc1882737u}, + {0x3f7fdb0fu, 0x3f7fdb0fu, 0x3f7fdb0fu, 0xbf801279u, 0xbf801279u}, + {0xbe511687u, 0xbe511687u, 0xbe511687u, 0xbe5166f4u, 0xbe5166f4u}, + {0xc1e56f13u, 0xc1e56f13u, 0x41e55ba5u, 0x41e55ba5u, 0xc1e56f13u}, + {0x439a1228u, 0x439a1228u, 0x439a1228u, 0xc3a5d2acu, 0xc3a5d2acu}, + {0x37f5fb14u, 0x37f5fb14u, 0x37f5fc0cu, 0x37f5fc0cu, 0x37f5fb14u}, + {0x7fd0a000u, 0x7fd0a000u, 0x7fd0a000u, 0x7fd0a000u, 0x7fd0a000u}, + {0xffc8ab86u, 0xffc8ab86u, 0x7fc8ab86u, 0x7fc8ab86u, 0xffc8ab86u}, + {0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u}, + {0xffc00000u, 0xffc00000u, 0xffc00000u, 0xffc00000u, 0xffc00000u}, + {0xb974e480u, 0xb974e480u, 0xb974e480u, 0xb974e480u, 0xb974e480u}, + {0x7f800000u, 0x7f800000u, 0x7f800000u, 0xff800000u, 0xff800000u}, + {0xff800000u, 0xff800000u, 0xff800000u, 0xff800000u, 0xff800000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u}, + {0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u}, + {0x7fc42000u, 0x7fc42000u, 0x7fc42000u, 0x7fc42000u, 0x7fc42000u}, + {0xbf052ae0u, 0xbf052ae0u, 0xbf052ae0u, 0xbf052ae0u, 0xbf052ae0u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x5f834da7u, 0x5f834da7u, 0x5f834da7u, 0xdf834da7u, 0xdf834da7u}, + {0x7f7fffffu, 0x7f7fffffu, 0x7f7fffffu, 0xff7fffffu, 0xff7fffffu}, + {0x42750eeeu, 0x42750eeeu, 0x42750eeeu, 0xc2750eeeu, 0xc2750eeeu}, + {0x43380748u, 0x43380748u, 0x43380748u, 0xc3380746u, 0xc3380746u}, + {0xffc00000u, 0xffc00000u, 0xffc00000u, 0xffc00000u, 0xffc00000u}, + {0xf540cddbu, 0xf540cddbu, 0x7540cddbu, 0x7540cddbu, 0xf540cddbu}, + {0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u}, + {0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0xffe00000u, 0xffe00000u}, + {0x3cb7c720u, 0x3cb7c720u, 0x3cb7c720u, 0x3cb7c720u, 0x3cb7c720u}, + {0xbf9bd158u, 0xbf9bd158u, 0xbf9bd158u, 0xbf9bd158u, 0xbf9bd158u}, + {0x565f49ebu, 0x565f49ebu, 0x565f49ebu, 0xd65f49ebu, 0xd65f49ebu}, + {0xbaf43c38u, 0xbaf43c38u, 0xbaf43c38u, 0xbaf43c38u, 0xbaf43c38u} +}; + +constexpr const char* AccumulatorNames[5] = {"v_dot2 op_sel src2", "v_dot2 op_sel_hi src2 cleared", "v_dot2 neg_hi src2", "v_dot2 neg_lo src2", "v_dot2 neg_lo and neg_hi src2"}; + constexpr const char* Names[10] = {"v_dot2c", "v_dot2", "v_dot2 neg_lo src0/src2 neg_hi src1", "v_dot2 neg_lo src1 neg_hi src0", "v_dot2 op_sel src0", "v_dot2 clamp", "v_dot2c inline 1.0", "v_dot2c literal", "v_dot2c dpp quad_perm", "v_dot2 op_sel src1"}; void Fill(std::uint32_t tid, std::uint32_t* words) { @@ -140,21 +183,9 @@ auto Compile(AgcDriver::VulkanDevice& device, std::span cod return ShaderRecompiler::Recompile(request); } -void CheckRefused(AgcDriver::VulkanDevice& device, std::uint32_t word0, const std::string& what) { - alignas(256) const std::array code{word0, 0x1c1a0b04u, 0xbf810000u}; - std::string refusal; - try { - static_cast(Compile(device, code)); - } catch (const std::exception& error) { - refusal = error.what(); - } - Require(refusal.find("v_dot2_f32_f16 accumulator op_sel, op_sel_hi and neg_hi are not implemented") != std::string::npos, what + " was not refused"); -} - -void Run(AgcDriver::VulkanDevice& device) { +void Run(AgcDriver::VulkanDevice& device, std::span code) { for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]); Output.fill(0xdeadbeefu); - const std::span code(Code); const auto result = Compile(device, code); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); @@ -167,6 +198,13 @@ void Check() { } } +void CheckAccumulator() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t* out = &Output[tid * Results]; + for (std::uint32_t i = 0; i < 5; ++i) Expect(tid, out[i], AccumulatorExpected[tid][i], AccumulatorNames[i]); + } +} + } int main() { @@ -177,11 +215,10 @@ int main() { std::puts("skipped, v_dot2_f32_f16 is computed in f64 and the device has no shaderFloat64"); return VulkanTestSkipped; } - Run(*device); + Run(*device, Code); Check(); - CheckRefused(*device, 0xcc13600au, "op_sel on the accumulator"); - CheckRefused(*device, 0xcc13440au, "neg_hi on the accumulator"); - CheckRefused(*device, 0xcc13000au, "op_sel_hi cleared on the accumulator"); + Run(*device, AccumulatorCode); + CheckAccumulator(); std::puts("v_dot2_f32_f16 tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp index 02ccb674b8..d3c9624463 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp @@ -2480,9 +2480,6 @@ RdnaInstruction DecodeRdnaVop3p(std::uint32_t programCounter, std::span 2u) { instruction.source2 = DecodeRdnaScalarSource(src2, programCounter); } - if (instruction.op == RdnaOpcode::VDot2F32F16 && ((((opSel | negHi) >> 2u) & 1u) != 0u || ((opSelHi >> 2u) & 1u) == 0u)) { - throw std::invalid_argument("VOP3P v_dot2_f32_f16 accumulator op_sel, op_sel_hi and neg_hi are not implemented"); - } if (instruction.op == RdnaOpcode::VFmaF32 || instruction.op == RdnaOpcode::VDot2F32F16 || isMadMixF16(instruction.op) || isPackedVop3p(instruction.op) || isVop3pIntegerSaturate(instruction.op)) { instruction.destination.clamp = clamp != 0u; } else if (clamp != 0u && instruction.op != RdnaOpcode::VPkMulLoU16) { @@ -2493,6 +2490,12 @@ RdnaInstruction DecodeRdnaVop3p(std::uint32_t programCounter, std::span Date: Wed, 21 Oct 2026 05:59:35 +0200 Subject: [PATCH 024/428] perf(agc): skip the queued-draw read check when a storage flush stores nothing StorageTexture::FlushPending asks DrawPipeline::HeldReads first on every call: it takes the draw pipeline's mutex and walks the read ranges of every queued draw. Most flushes come from sampled-texture lookups and storage refreshes over memory without pending results. Those return false without storing or publishing anything whether a queued draw holds the range or not, so FlushPending now returns before the check when nothing would be stored: no pending image with a unit in the range, none being flushed, and no unit shadow over it when the scope publishes. A flush that has something to store still asks HeldReads first. DrawPipeline exposes its held-read query count for the test. AI-assisted: yes --- .../Execution/include/Driver/Draw/DrawPipeline.hpp | 1 + .../Execution/src/Driver/Draw/DrawPipeline.cpp | 4 ++++ .../prx/libSceAgcDriver/Graphics/src/Texture.cpp | 12 ++++++++++++ .../tests/execution/QueuedIndexReadOrder.cpp | 5 +++++ 4 files changed, 22 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp index 9ca8b03dd5..6f3bff3cee 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp @@ -41,6 +41,7 @@ public: bool Overlaps(std::uint64_t address, std::size_t bytes); std::optional PendingLabel(std::uint64_t address, std::size_t bytes); static bool HeldReads(std::uint64_t address, std::size_t bytes); + static std::uint64_t HeldReadQueries(); void ReleaseRunningReads(); private: diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp index 9ba7b4a1db..3bea57bf94 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp @@ -237,6 +237,10 @@ bool DrawPipeline::HeldReads(std::uint64_t address, std::size_t bytes) { return false; } +std::uint64_t DrawPipeline::HeldReadQueries() { + return heldReadQueries.load(std::memory_order_relaxed); +} + void DrawPipeline::ReleaseRunningReads() { std::lock_guard lock(mutex); if (!items.empty()) items.front().reads.clear(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index eb824c85d1..eb1ccc049f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3670,6 +3670,18 @@ bool StorageTexture::pendingUnitInside(std::uint64_t address, std::size_t bytes) bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except, const char* reason, PublishScope scope, bool* published) { static_cast(FlushDepthPlanes(address, bytes)); if (published != nullptr) *published = false; + const auto nothingToStore = [&] { + auto& pending = Pending(); + if (pending.flushingCount.load(std::memory_order_acquire) != 0) return false; + if (!pending.textures.SurelyClear(address, bytes)) { + std::lock_guard lock(pending.mutex); + const auto* exempt = refreshing; + if (pending.textures.MayOverlap(address, bytes) && std::any_of(pending.textures.begin(), pending.textures.end(), [&](const StorageTexture* texture) { return texture != except && texture != exempt && texture->overlaps(address, bytes) && texture->pendingUnitInside(address, bytes); })) return false; + if (pending.flushingCount.load(std::memory_order_acquire) != 0) return false; + } + return scope == PublishScope::None || !AnyShadowedOverlaps(address, bytes); + }; + if (nothingToStore()) return false; if (AgcDriver::DriverDetail::DrawPipeline::HeldReads(address, bytes)) { queuedReadHeld.fetch_add(1, std::memory_order_relaxed); return false; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/QueuedIndexReadOrder.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/QueuedIndexReadOrder.cpp index acc2b1617a..b0fca21910 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/QueuedIndexReadOrder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/QueuedIndexReadOrder.cpp @@ -373,6 +373,11 @@ int main() { AgcDriverWaitIdle_nid_postfix(); Require(AgcDriver::Graphics::StorageTexture::AnyPending(), "queued index read: the image write left no pending result to order against"); Require(IndicesIntact(), "queued index read: the image's results reached guest memory before any consumer asked"); + const auto copiedAddress = static_cast(reinterpret_cast(Copied)); + const auto heldQueries = AgcDriver::DriverDetail::DrawPipeline::HeldReadQueries(); + Require(!AgcDriver::Graphics::StorageTexture::FlushPending(copiedAddress, PoolBytes), "queued index read: a flush over memory without pending results stored something"); + Require(AgcDriver::DriverDetail::DrawPipeline::HeldReadQueries() == heldQueries, "queued index read: a flush with nothing to store asked the draw pipeline for held reads"); + Require(IndicesIntact(), "queued index read: a flush over other memory stored the image's results"); GateUntilQueued(); std::fill_n(ColorMemory, ColorBytes, std::byte{0}); Submit(Draw(Vertices.data(), Vertices.size()), "the indexed draw"); From a4079e9d4cf9bdb84c09d29a67ae509c55b03fad Mon Sep 17 00:00:00 2001 From: maximaxi01 Date: Mon, 19 Oct 2026 20:52:22 -0400 Subject: [PATCH 025/428] fix(libSceFont): select the Hangul Light system font --- core/libs/prx/libSceFont/src/SystemFont.cpp | 2 +- core/libs/tests/GuestSystemFont.cpp | 14 ++++++++++++++ docs/dev/debt/libSceFont.md | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceFont/src/SystemFont.cpp b/core/libs/prx/libSceFont/src/SystemFont.cpp index 5bcfb0789e..6f51af20c9 100644 --- a/core/libs/prx/libSceFont/src/SystemFont.cpp +++ b/core/libs/prx/libSceFont/src/SystemFont.cpp @@ -60,7 +60,7 @@ constexpr std::array FAMILY_FILES = {{ {"NotoSansCJK-Light.ttc", "NotoSansCJK-Regular.ttc", "NotoSansCJK-Medium.ttc", "NotoSansCJK-Bold.ttc"}, 0}, {{"DFHEI5-SONY.ttf", "DFHEI5-SONY.ttf", "DFHEI5-SONY.ttf", "DFHEI5-SONY.ttf"}, {"NotoSansCJK-Light.ttc", "NotoSansCJK-Regular.ttc", "NotoSansCJK-Medium.ttc", "NotoSansCJK-Bold.ttc"}, 2}, - {{"SCEPS4Yoongd-Medium.otf", "SCEPS4Yoongd-Medium.otf", "SCEPS4Yoongd-Medium.otf", "SCEPS4Yoongd-Bold.otf"}, + {{"SCEPS4Yoongd-Light.otf", "SCEPS4Yoongd-Medium.otf", "SCEPS4Yoongd-Medium.otf", "SCEPS4Yoongd-Bold.otf"}, {"NotoSansCJK-Light.ttc", "NotoSansCJK-Regular.ttc", "NotoSansCJK-Medium.ttc", "NotoSansCJK-Bold.ttc"}, 1}, {{"SSTArabic-Light.otf", "SSTArabic-Roman.otf", "SSTArabic-Medium.otf", "SSTArabic-Bold.otf"}, {"NotoSansArabic-Light.ttf", "NotoSansArabic-Regular.ttf", "NotoSansArabic-Medium.ttf", "NotoSansArabic-Bold.ttf"}, 0}, diff --git a/core/libs/tests/GuestSystemFont.cpp b/core/libs/tests/GuestSystemFont.cpp index f7f07156cf..c2509e12ed 100644 --- a/core/libs/tests/GuestSystemFont.cpp +++ b/core/libs/tests/GuestSystemFont.cpp @@ -341,6 +341,20 @@ int main() { Require(sceFontUnbindRenderer(mixed) == SCE_FONT_OK); Require(sceFontDestroyRenderer(&renderer) == SCE_FONT_OK); + const auto hangulFonts = root / "hangul-fonts"; + std::filesystem::create_directories(hangulFonts); + for (const auto* name : {"SST-Light.otf", "SST-Roman.otf", "SST-Bold.otf", "SSTJpPro-Regular.otf"}) { + WriteFile(hangulFonts / name, SquareGlyphFont('A', 600)); + } + WriteFile(hangulFonts / "SCEPS4Yoongd-Light.otf", SquareGlyphFont(0xAC00, 400)); + WriteFile(hangulFonts / "SCEPS4Yoongd-Medium.otf", SquareGlyphFont(0xAC00, 650)); + WriteFile(hangulFonts / "SCEPS4Yoongd-Bold.otf", SquareGlyphFont(0xAC00, 800)); + SetFontDirectory(hangulFonts); + Require(SetAdvanceOf(library, 0x1A072543u, 0xAC00) == 40.0f); + Require(SetAdvanceOf(library, 0x1A082543u, 0xAC00) == 40.0f); + Require(SetAdvanceOf(library, 0x18072444u, 0xAC00) == 65.0f); + Require(SetAdvanceOf(library, 0x18072447u, 0xAC00) == 80.0f); + for (FontHandle handle : {font, instance, substitute, memoryFont, mixed, mixedInstance}) Require(sceFontCloseFont(handle) == SCE_FONT_OK); Require(sceFontDestroyLibrary(&library) == SCE_FONT_OK); Require(allocations == 0); diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index 5c3d5ad062..a7d002fbd8 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -18,7 +18,7 @@ ### Functional -- [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - the other script files of a set are chosen from its type, not measured on a console: Vietnamese for low-byte bit 0x10, Thai for script bit 0x10, Japanese for 0x04, simplified Chinese for 0x80 (before Japanese with 0x08), Hangul for 0x20 and Arabic for low-byte bit 0x80, as the SDK set names (`..._JP_AR`, `..._GBCJK_TH`) listed by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/font/font_internal.cpp) suggest; the file order and the Hangul file name (`SCEPS4Yoongd-Medium.otf`, `SCEPS4Yoongd-Bold.otf`) also come from shadPS4. A character the primary file lacks is taken from the first of them that has it; kerning between two files is 0, and `sceFontGetCharGlyphCode` and `sceFontGetFontGlyphsCount` answer for the primary file only +- [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - the other script files of a set and their priority are inferred, not measured on a console: Vietnamese for low-byte bit 0x10, Thai for script bit 0x10, Japanese for 0x04, simplified Chinese for 0x80 (before Japanese with 0x08), Hangul for 0x20 and Arabic for low-byte bit 0x80. The SDK set names (`..._JP_AR`, `..._GBCJK_TH`) and Hangul aliases (`SCEPS4Yoongd-Light.otf`, `SCEPS4Yoongd-Medium.otf`, `SCEPS4Yoongd-Bold.otf`) come from [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b45b289761ffcb4feed44df5f51b4bb5b5c0dfdc/src/core/libraries/font/font_internal.cpp); the Vietnamese bit rule and the priority used here are our inferences, not its fallback implementation. A character the primary file lacks is taken from the first of them that has it; kerning between two files is 0, and `sceFontGetCharGlyphCode` and `sceFontGetFontGlyphsCount` answer for the primary file only - [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - the console file names are those of the PS4 firmware; the names of the PS5-only JG2 sets (`0x1A......`) are unknown, so they use the SSTJpPro files. When a console file is missing, an openly licensed substitute (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans Arabic, Noto Sans CJK with its Korean face for Hangul) is loaded instead; its metrics differ, so text width, line height and wrapping differ from the console. System font faces are loaded by FreeType in host memory, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. - [sceFontGetCharGlyphMetrics, sceFontGetRenderCharGlyphMetrics](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) report the vertical metrics the way the [FreeType driver](../../../core/libs/prx/libSceFontFt/src/Driver.cpp) reports them for a rendered glyph, not confirmed on a title: FreeType's vertical advance, and for a face without vertical metrics a bearing of half the horizontal advance left of the horizontal bearing and of half the advance left over by the glyph height. - [Font writing lines](../../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - `sceFontWritingLineWritesOrder`, `sceFontWritingLineRefersRenderStep`, `sceFontWritingLineGetRenderMetrics`, `sceFontWritingLineGetOrderingSpace` and `sceFontWritingLineClear` follow the PS4 11.00 `libSceFont.sprx` (firmware module, read from its disassembly and compared call for call on random sequences): runs are linked in visual order by embedding level, the space at each spaced join is summed for the line, steps walk the visual order from the left (form `0x12`) or from the right (form `0x11`) and the render metrics grow as steps are consumed. The PS5 library was not measured. Not from that module: form `0x10` is accepted and treated as left to right and a detail with id `0x0FD5` is accepted (the PS4 library rejects both); the memory argument of `sceFontCreateWritingLine` and the rest of the detail are not read (the PS4 library validates its library object there); an unknown writing form, another detail id, a null line pointer on creation, unknown and null handles (except in `sceFontWritingLineRefersRenderStep`, which returns null for a null line) and a null output of `sceFontWritingLineGetRenderMetrics` throw instead of returning the PS4 error codes, which are not confirmed for the PS5; `sceFontDestroyWritingLine` was not compared. Lines are allocated on the host, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. From 320a4b6a1946d1e4638c95f1020ee59641c0f4ef Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Wed, 21 Oct 2026 11:35:33 +0300 Subject: [PATCH 026/428] fix(agc): restore partialThreads registration variant and bound dispatch look-ahead --- .../src/Driver/Dispatch/DispatchAhead.cpp | 11 +---- .../src/Driver/Packets/PacketExecution.cpp | 6 +-- .../src/Driver/Shaders/ShaderRegistry.cpp | 5 ++- .../tests/execution/PreparedShaders.cpp | 40 ++++++++++++------- 4 files changed, 33 insertions(+), 29 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp index e32182b7cb..fd7afb5b6c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp @@ -105,10 +105,7 @@ bool OverlapsLabel(std::span regions, cons } void Driver::resolveGroupAhead(const Submission& submission, const QueueState& live, std::size_t from) { - static const bool allQueues = [] { - const char* text = std::getenv("APS5_GROUP_CAPTURE_ALL_QUEUES"); - return text == nullptr || (text[0] != '0' && text[0] != '\0'); - }(); + static const bool allQueues = std::getenv("APS5_GROUP_CAPTURE_ALL_QUEUES") != nullptr; if (!allQueues && submission.queue == 0) return; auto& resolved = resolvedAhead(); const auto localDevice = device.Load(); @@ -145,7 +142,7 @@ void Driver::resolveGroupAhead(const Submission& submission, const QueueState& l const auto packet = std::span(commands).subspan(scan, count); const auto opcode = (header >> 8u) & 0xffu; const auto next = scan + count; - if (header == FlipPacketHeader || opcode == 0x22 || opcode == 0x3f) break; + if (header == FlipPacketHeader || header == RenderingWaitPacketHeader || opcode == 0x22 || opcode == 0x3f || opcode == 0x3c || opcode == 0x93) break; if (opcode == 0x15 || opcode == 0x16) { if (count < (opcode == 0x15 ? 5u : 3u)) break; try { @@ -268,10 +265,6 @@ void Driver::resolveGroupAhead(const Submission& submission, const QueueState& l if (!label) break; NoteLabel(labels, label->address, label->Bytes()); writes.emplace_back(label->address, label->address + label->Bytes().size()); - } else if (Pm4::IndirectRegisterOpcode(opcode)) { - const auto found = submission.registerLists.find(cursor); - if (found == submission.registerLists.end() || !Pm4::IndirectRegistersValid(found->second)) break; - try { Pm4::ExecuteIndirectRegisters(packet, found->second, scratch); } catch (const std::exception&) { break; } } else if (opcode != 0x3f && opcode != 0x42 && opcode != 0x46 && opcode != 0x58) { if (Pm4::AccessesMemory(header)) break; try { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index 5a5a3e1ef8..4a4e4e457f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -226,7 +226,6 @@ void Driver::execute(const Submission& submission) { struct ClearAhead { ~ClearAhead() { clearResolvedAhead(); } } clearAhead; - std::size_t aheadCursor = 0; for (std::size_t cursor = 0; cursor < submission.commands.size();) { if (packetEpoch()) bumpEpoch(&EpochBumps::packets); CheckFailure(); @@ -391,10 +390,9 @@ void Driver::execute(const Submission& submission) { packetCounters.CountDispatch(); landPendingWaits(submission.queue); currentPacketOffset() = cursor; - if (gateOpened() || (resolvedAhead().empty() && cursor >= aheadCursor)) { + if (gateOpened()) { gateOpened() = false; - aheadCursor = nextCursor; - if (!pipelineDispatch() || opcode == 0x16 || !DrawPipeline::Active() || submission.queue != 0) { + if (!pipelineDispatch() || submission.queue != 0) { if (pipelined && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, opcode); timed(&WorkerProfile::dispatchMs, [&] { resolveGroupAhead(submission, queue, cursor); }); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index badf48b3c0..a478375483 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1051,7 +1051,10 @@ std::vector PrepareRegistered(RegisteredPreparation& pla APS5_LOG_OUT("shader library prebuild skipped: %s", error.what()); } } - if (request.context.pixel) { + if (request.context.compute) { + request.context.compute->partialThreads = {1, 1, 1}; + append(); + } else if (request.context.pixel) { request.layout.pushConstantSizeBytes = 0; append(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index 00c0066a6f..b17d4eb276 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -1263,27 +1263,29 @@ void RelocatedHeaders() { void DispatchAheadPrewarm() { alignas(256) std::array pendingCode{0xbf810000u}; - alignas(256) std::array lazyCode{0xbf810000u}; + alignas(256) std::array lazyCode{}; + lazyCode[0] = 0xbf810000u; + lazyCode[64] = 0xbf810000u; struct Header { Shader shader{}; std::array registers{}; ShaderSpecialRegs specials{}; } pendingHeader, lazyHeader; - const auto fillHeader = [](Header& header, const std::array& code) { - const auto address = reinterpret_cast(code.data()); + const auto fillHeader = [](Header& header, const void* code, std::size_t codeBytes) { + const auto address = reinterpret_cast(code); header.shader.file_header = 0x34333231u; header.shader.version = 0x18; header.shader.header_size = sizeof(Header); - header.shader.shader_size = sizeof(code); - header.shader.code = code.data(); + header.shader.shader_size = static_cast(codeBytes); + header.shader.code = const_cast(code); header.shader.sh_registers = header.registers.data(); header.shader.num_sh_registers = header.registers.size(); header.shader.specials = &header.specials; header.specials.dispatch_modifier = 0x8000; header.registers = {{{0x20c, static_cast(address >> 8u)}, {0x20d, static_cast(address >> 40u)}, {0x207, 1}, {0x208, 1}, {0x209, 1}, {0x212, 0}, {0x213, 2}}}; }; - fillHeader(pendingHeader, pendingCode); - fillHeader(lazyHeader, lazyCode); + fillHeader(pendingHeader, pendingCode.data(), sizeof(pendingCode)); + fillHeader(lazyHeader, lazyCode.data(), sizeof(lazyCode)); AgcDriverRegisterShader_nid_postfix(&pendingHeader.shader); AgcDriverRegisterShader_nid_postfix(&lazyHeader.shader); AgcDriver::DriverDetail::AwaitRegistrationPreparation(); @@ -1292,11 +1294,10 @@ void DispatchAheadPrewarm() { const auto pendingSnapshot = AgcDriver::DriverDetail::RegisteredHeader(*registry, &pendingHeader.shader); const auto lazySnapshot = AgcDriver::DriverDetail::RegisteredHeader(*registry, &lazyHeader.shader); Require(pendingSnapshot != nullptr && lazySnapshot != nullptr, "dispatch ahead prewarm test published no snapshots"); - std::vector savedEntries; { std::lock_guard lock(pendingSnapshot->prepared->mutex); - savedEntries = std::move(pendingSnapshot->prepared->entries); pendingSnapshot->prepared->entries.clear(); + pendingSnapshot->prepared->demanded.store(false, std::memory_order_release); pendingSnapshot->prepared->pending = true; } { @@ -1304,19 +1305,28 @@ void DispatchAheadPrewarm() { lazySnapshot->prepared->entries.clear(); lazySnapshot->prepared->pending = false; } - auto settle = std::async(std::launch::async, SettleLater, std::ref(*pendingSnapshot->prepared), std::move(savedEntries), nullptr); - alignas(16) const std::array indirectPairs{0x20cu, lazyHeader.registers[0].value, 0x20du, lazyHeader.registers[1].value, 0x240u, 7u}; + alignas(256) static const std::array invalidCode{0xbf9f0000u}; + const auto invalidAddress = reinterpret_cast(invalidCode.data()); + const auto lazyOffsetAddress = reinterpret_cast(lazyCode.data() + 64); + alignas(16) const std::array indirectPairs{0x20cu, static_cast(lazyOffsetAddress >> 8u), 0x20du, static_cast(lazyOffsetAddress >> 40u), 0x240u, 7u}; const auto pairsAddress = reinterpret_cast(indirectPairs.data()); std::vector commands; for (const auto reg : pendingHeader.registers) commands.insert(commands.end(), {0xc0017600u, reg.offset, reg.value}); - commands.insert(commands.end(), {0xc0031500u, 1, 1, 1, 0x8041u, 0xffff1000u}); + commands.insert(commands.end(), {0xc0017600u, 0x20cu, static_cast(invalidAddress >> 8u), 0xc0017600u, 0x20du, static_cast(invalidAddress >> 40u), 0xc0031500u, 1, 1, 1, 0x8041u, 0xffff1000u}); + commands.insert(commands.end(), {0xc0017600u, 0x20cu, pendingHeader.registers[0].value, 0xc0017600u, 0x20du, pendingHeader.registers[1].value, 0xc0031500u, 1, 1, 1, 0x8041u}); commands.insert(commands.end(), {0xc0036300u, static_cast(pairsAddress), static_cast(pairsAddress >> 32u), 0x80000000u, 3u}); commands.insert(commands.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; Require(sceAgcDriverSubmitAcb(0x20, &packet) == 0, "dispatch ahead prewarm submit failed"); - AgcDriverWaitIdle_nid_postfix(); - settle.get(); - AgcDriverShutdown_nid_postfix(); + ExpectFailure([] { AgcDriverWaitIdle_nid_postfix(); }, "s_code_end"); + AgcDriver::DriverDetail::AwaitRegistrationPreparation(); + AgcDriver::DriverDetail::AwaitLibraryPrebuilds(); + Require(pendingSnapshot->prepared->demanded.load(std::memory_order_acquire), "dispatch ahead did not prioritize the pending shader"); + { + std::lock_guard lock(lazySnapshot->prepared->mutex); + Require(lazySnapshot->prepared->entries.size() == 1 && lazySnapshot->prepared->entries.front().codeOffset == 64 && lazySnapshot->prepared->entries.front().handle != nullptr, "dispatch ahead did not prewarm the lazy shader for its dispatched offset"); + } + ExpectFailure([] { AgcDriverShutdown_nid_postfix(); }, "s_code_end"); } } From 6ab912f8188fde91f05e9d513a449f6e83d556ca Mon Sep 17 00:00:00 2001 From: blu <131330772+ycbuzi@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:40:49 -0700 Subject: [PATCH 027/428] feat(libkernel): export _sigintr and let signal honour it The public SDK's libkernel, libkernel_sys and libkernel_web stub tables declare _sigintr, the sigset_t that FreeBSD's libc shares between signal and siginterrupt, and no built library provided it (#8846). libkernel now exports the 16-byte object, zero at start, and signal records a handler with SA_RESTART unless its signal is in the set, as lib/libc/gen/signal.c does. siginterrupt, which fills the set, is a libSceLibcInternal export; libc does not link libkernel and cannot reach the dispositions, so it stays out and a debt entry for signal says so. Co-Authored-By: Claude Fable 5.1 --- .../libs/prx/libkernel/System/src/Signals.cpp | 4 +++- core/libs/tests/GuestSignals.cpp | 21 +++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 231d3d2643..2a60f977c7 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -32,6 +32,8 @@ struct GuestSigaction { }; static_assert(sizeof(GuestSigaction) == 32 && offsetof(GuestSigaction, flags) == 8 && offsetof(GuestSigaction, mask) == 12); +extern "C" GuestSignalSet _sigintr_nid_postfix{}; + namespace { using GuestHandler = void (APS5_VABI *)(int); constexpr int MaxSignal = 128; @@ -481,7 +483,7 @@ GuestHandler APS5_VABI signal_nid_postfix(int guest, GuestHandler handler) { const auto invalid = reinterpret_cast(static_cast(-1)); if (guest < 1 || guest > MaxSignal || ((guest == GuestSigkill || guest == GuestSigstop) && handler != nullptr)) { *__error_nid_postfix() = 22; return invalid; } std::lock_guard lock(registration); - const GuestSigaction action{reinterpret_cast(handler), SaRestart, {}}; + const GuestSigaction action{reinterpret_cast(handler), SignalInSet(_sigintr_nid_postfix, guest) ? 0 : SaRestart, {}}; if (!Install(guest, action)) { *__error_nid_postfix() = 22; return invalid; } const auto previous = reinterpret_cast(dispositions[guest].handler); dispositions[guest] = action; diff --git a/core/libs/tests/GuestSignals.cpp b/core/libs/tests/GuestSignals.cpp index 5183ec41b0..69409b2383 100644 --- a/core/libs/tests/GuestSignals.cpp +++ b/core/libs/tests/GuestSignals.cpp @@ -36,6 +36,7 @@ struct GuestSigaction { GuestSignalSet mask; }; static_assert(sizeof(GuestSigaction) == 32); +extern GuestSignalSet _sigintr_nid_postfix; static bool Same(const GuestSigaction& left, const GuestSigaction& right) { return left.handler == right.handler && left.flags == right.flags && std::memcmp(&left.mask, &right.mask, sizeof(left.mask)) == 0; } @@ -365,4 +366,24 @@ int main() { Require(sigpending_nid_postfix(&pendingIgnored) == 0 && pendingIgnored.bits[0] == 0); Require(sigprocmask_nid_postfix(3, &noneBlocked, nullptr) == 0); Require(sigaction_nid_postfix(30, &restored, nullptr) == 0); + Require(_sigintr_nid_postfix.bits[0] == 0 && _sigintr_nid_postfix.bits[1] == 0 && _sigintr_nid_postfix.bits[2] == 0 && + _sigintr_nid_postfix.bits[3] == 0); + GuestSigaction interruptible{}; + Require(signal_nid_postfix(30, Callback) == nullptr); + Require(sigaction_nid_postfix(30, nullptr, &interruptible) == 0 && interruptible.flags == 0x2); + _sigintr_nid_postfix.bits[0] = 1u << 29; + Require(signal_nid_postfix(30, Callback) == Callback); + Require(sigaction_nid_postfix(30, nullptr, &interruptible) == 0 && + interruptible.handler == reinterpret_cast(Callback) && interruptible.flags == 0); + signal_nid_postfix(31, Callback); + Require(sigaction_nid_postfix(31, nullptr, &interruptible) == 0 && interruptible.flags == 0x2); + _sigintr_nid_postfix.bits[3] = 1u << 31; + Require(signal_nid_postfix(128, Callback) == nullptr); + Require(sigaction_nid_postfix(128, nullptr, &interruptible) == 0 && interruptible.flags == 0); + _sigintr_nid_postfix.bits[0] = 0; + _sigintr_nid_postfix.bits[3] = 0; + Require(signal_nid_postfix(30, Callback) == Callback); + Require(sigaction_nid_postfix(30, nullptr, &interruptible) == 0 && interruptible.flags == 0x2); + Require(signal_nid_postfix(30, nullptr) == Callback && signal_nid_postfix(31, nullptr) == Callback && + signal_nid_postfix(128, nullptr) == Callback); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6384b00c9f..b820c745ad 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -207,6 +207,7 @@ - [pause, sigsuspend, sigwait, sigwaitinfo](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) take the pending set of `sigtimedwait` instead of throwing. `pause` waits for a pending signal that is not blocked and whose disposition can be delivered, runs its handler on the calling thread and returns -1 with `EINTR`; an ignored signal is discarded and the wait continues, and it never returns for a signal whose handler cannot run. A signal whose registered handler has `SA_SIGINFO` still throws, and one whose disposition is the default action and that has no host equivalent (every signal except 2, 4, 6, 8, 11 and 15) is neither delivered nor discarded: it stays pending until a wait takes it, where FreeBSD runs the default action, which terminates the process for most of them. A pending `SIGALRM` with the default disposition exits the process with status 142 once it is unblocked, as FreeBSD's default action does. PPSA68091, PPSA99169, PPSA99420 and PPSA97358 import them and fail to load without them. PPSA68091 calls `pause` at one site (0x8F7266), in a loop that waits while a global holds another thread's `errno` address. PPSA99169 only stores the address of `pause` in a table of symbol addresses (0x30DB1). PPSA99420 calls `pause` at 0x3BD80F6 and 0x3C9E220, `sigwait` at 0x3C9E4C7 and `sigwaitinfo` at 0x3C9E588. PPSA97358 calls `sigwait` from QEMU's `sigwait_compat` thread ([compatfd.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/compatfd.c#L28-L53)), which `qemu_signal_init` ([main-loop.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/main-loop.c#L110-L147)) starts for `SIGIO`, `SIGALRM` and `SIGBUS` when the main loop is created, so it reaches the wait at start-up, and from the vCPU thread of the dummy accelerator ([dummy-cpus.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/accel/dummy-cpus.c#L48-L56)). `sigsuspend` swaps the process-wide mask of `_sigprocmask` for its argument, waits as `pause` does through the same helper, then restores the mask through `_sigprocmask`, and returns -1 with `EINTR`; a null set throws, as the console faults in libthr instead of returning `EFAULT`. All four words of the argument replace the mask and are restored afterwards, as in `_sigprocmask`. Java 25 LAN / DNS (PPSA99869, #5501) imports it and fails to load without it, after `sysctlnametomib`, `fegetenv` and `mincore`. None of the five waits is a cancellation point - [getsid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns the calling process ID for pid 0 and the calling process, and `ESRCH` for other process IDs, assuming the single guest process is its own session leader. PPSA74126 imports it and fails to load without it - [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating +- [signal](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records its handler with `SA_RESTART` unless the signal is in the exported `_sigintr` set, as FreeBSD's libc does (`lib/libc/gen/signal.c`). `siginterrupt`, which fills that set, is a libSceLibcInternal export and is not provided: libc does not link libkernel and cannot reach the dispositions, so only a guest that writes the set itself clears `SA_RESTART` this way. - [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console - [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console From 524dcd4baeb2cc3191e015d74aa89f187c22b41d Mon Sep 17 00:00:00 2001 From: blu <131330772+ycbuzi@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:53:14 -0700 Subject: [PATCH 028/428] feat(libc): implement siginterrupt through the sigaction libkernel registers siginterrupt is a libSceLibcInternal export (the public SDK's stub table has it there, not in the kernel tables) and FreeBSD 11.4's lib/libc/gen/siginterrupt.c reads the signal's disposition, sets or clears the signal's bit in _sigintr and SA_RESTART, and installs the disposition again. libc cannot call libkernel, so, as the pthread hooks do, libc keeps a registered sigaction and the address of the _sigintr set, and libkernel registers both from a static initializer when it loads. A call before that throws; the set is written without libkernel's registration lock, as FreeBSD's libc does. Builds on #8866, which adds the set. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libc/include/General.hpp | 3 ++ core/libs/prx/libc/src/Process.cpp | 40 +++++++++++++++++++ .../libs/prx/libkernel/System/src/Signals.cpp | 9 +++++ core/libs/tests/GuestSignals.cpp | 21 ++++++++++ docs/dev/debt/libc.md | 1 + docs/dev/debt/libkernel.md | 2 +- 6 files changed, 75 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index 6dade90518..8d350157ad 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -58,6 +58,9 @@ extern "C" void RegisterPthreadCreate_nid_no_patch(PthreadCreateFn_nid_no_patch extern "C" void RegisterPthreadJoin_nid_no_patch(PthreadJoinFn_nid_no_patch fn); extern "C" void RegisterPthreadDetach_nid_no_patch(PthreadDetachFn_nid_no_patch fn); +using SigactionFn_nid_no_patch = int (APS5_VABI*)(int, const void*, void*); +extern "C" void RegisterSigaction_nid_no_patch(SigactionFn_nid_no_patch fn, std::uint32_t* interruptSet); + #define APS5_INVALID_ARG_EX throw std::invalid_argument(std::string(__func__) + ": invalid argument") #define APS5_DUMMY_FUN \ diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index 9d2f6909b0..c200af881d 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -281,3 +282,42 @@ int APS5_VABI daemon_nid_postfix(int noChdir, int noClose) { } } + +namespace { +constexpr int SaRestart = 0x2; +std::atomic registeredSigaction{nullptr}; +std::atomic registeredInterruptSet{nullptr}; + +struct InterruptSigaction { + std::uintptr_t handler; + int flags; + std::uint32_t mask[4]; +}; +static_assert(sizeof(InterruptSigaction) == 32 && offsetof(InterruptSigaction, flags) == 8 && offsetof(InterruptSigaction, mask) == 12); +} + +extern "C" { + +void RegisterSigaction_nid_no_patch(SigactionFn_nid_no_patch fn, std::uint32_t* interruptSet) { + registeredInterruptSet.store(interruptSet, std::memory_order_release); + registeredSigaction.store(fn, std::memory_order_release); +} + +int APS5_VABI siginterrupt_nid_postfix(int guest, int flag) { + const auto sigaction = registeredSigaction.load(std::memory_order_acquire); + auto* const interruptSet = registeredInterruptSet.load(std::memory_order_acquire); + if (sigaction == nullptr || interruptSet == nullptr) throw std::runtime_error("siginterrupt: libkernel has not registered sigaction"); + InterruptSigaction action{}; + if (sigaction(guest, nullptr, &action) != 0) return -1; + const auto index = static_cast(guest - 1); + if (flag) { + interruptSet[index >> 5] |= 1u << (index & 31); + action.flags &= ~SaRestart; + } else { + interruptSet[index >> 5] &= ~(1u << (index & 31)); + action.flags |= SaRestart; + } + return sigaction(guest, &action, nullptr); +} + +} diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 2a60f977c7..aefac53392 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -665,3 +665,12 @@ int APS5_VABI _is_signal_return_nid_postfix(std::uint64_t programCounter) { } } + +namespace { +struct RegisterSignalsOnLoad { + RegisterSignalsOnLoad() { + RegisterSigaction_nid_no_patch(reinterpret_cast(sigaction_nid_postfix), _sigintr_nid_postfix.bits); + } +}; +const RegisterSignalsOnLoad gRegisterSignals; +} diff --git a/core/libs/tests/GuestSignals.cpp b/core/libs/tests/GuestSignals.cpp index 69409b2383..e37eb6b69a 100644 --- a/core/libs/tests/GuestSignals.cpp +++ b/core/libs/tests/GuestSignals.cpp @@ -29,6 +29,7 @@ int APS5_VABI sigsuspend_nid_postfix(const GuestSignalSet*); int APS5_VABI _is_signal_return_nid_postfix(std::uint64_t); unsigned int APS5_VABI GuestAlarm_nid_no_patch(unsigned int); int APS5_VABI sigpending_nid_postfix(GuestSignalSet*); +int APS5_VABI siginterrupt_nid_postfix(int, int); } struct GuestSigaction { std::uintptr_t handler; @@ -386,4 +387,24 @@ int main() { Require(sigaction_nid_postfix(30, nullptr, &interruptible) == 0 && interruptible.flags == 0x2); Require(signal_nid_postfix(30, nullptr) == Callback && signal_nid_postfix(31, nullptr) == Callback && signal_nid_postfix(128, nullptr) == Callback); + Require(signal_nid_postfix(30, Callback) == nullptr); + Require(siginterrupt_nid_postfix(30, 1) == 0 && _sigintr_nid_postfix.bits[0] == (1u << 29)); + Require(sigaction_nid_postfix(30, nullptr, &interruptible) == 0 && + interruptible.handler == reinterpret_cast(Callback) && interruptible.flags == 0); + Require(signal_nid_postfix(30, Callback) == Callback); + Require(sigaction_nid_postfix(30, nullptr, &interruptible) == 0 && interruptible.flags == 0); + Require(siginterrupt_nid_postfix(30, 0) == 0 && _sigintr_nid_postfix.bits[0] == 0); + Require(sigaction_nid_postfix(30, nullptr, &interruptible) == 0 && + interruptible.handler == reinterpret_cast(Callback) && interruptible.flags == 0x2); + Require(siginterrupt_nid_postfix(128, 1) == 0 && _sigintr_nid_postfix.bits[3] == (1u << 31)); + Require(sigaction_nid_postfix(128, nullptr, &interruptible) == 0 && interruptible.handler == 0 && interruptible.flags == 0); + Require(siginterrupt_nid_postfix(128, 0) == 0 && _sigintr_nid_postfix.bits[3] == 0); + Require(sigaction_nid_postfix(128, nullptr, &interruptible) == 0 && interruptible.flags == 0x2); + for (const int badSignal : {0, -1, 129}) { + *__error_nid_postfix() = 0; + Require(siginterrupt_nid_postfix(badSignal, 1) == -1 && *__error_nid_postfix() == 22); + } + Require(_sigintr_nid_postfix.bits[0] == 0 && _sigintr_nid_postfix.bits[3] == 0); + Require(signal_nid_postfix(30, nullptr) == Callback); + Require(sigaction_nid_postfix(128, &restored, nullptr) == 0); } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index e3c4207769..f58ae64a46 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -133,3 +133,4 @@ - [fopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - returns the errno of the failed open, as C11 K.3.5.2.1 leaves the nonzero value open and Microsoft's CRT returns errno; the console's Dinkumware `fopen_s` is not verified - [ios_base standard streams](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) `_Addstd` queues the stream in the standard stream table of Dinkumware [ios.cpp](https://github.com/ganboing/crt.vc12/blob/master/src/ios.cpp) (`stdstr`, `stdopens`, `_Addstd`, `_Ios_base_dtor`): it takes the first of slots 1 to 7 that is free or already holds the stream, and slot 8 (`NSTDSTR`) when none is, so slot 8 is shared by every stream past the first seven and registration never fails. It stores the 1-based slot index in the guest `IosBase::standardStream` (Dinkumware's `_Stdstr`, where 0 means not a standard stream) and counts the stream objects registered per slot; registering the same stream again reuses its slot and raises its count. Measured on a PS5 (firmware 13.60) from a homebrew payload calling the system `libSceLibcInternal` export: in a process whose own standard streams already hold slots 1 to 7, twelve distinct streams and a repeated one all get 8, written as a 64-bit value at offset 0x8 with no other byte of the object touched; the assignment of slots 1 to 7 to fresh streams is not observable there and follows VC12. The counts are `int`, not VC12's `char`, and the registration and the destructor lock the table where VC12 locks only `_Addstd`; a slot index past the table throws in the destructor instead of indexing out of bounds. The destructor decrements its slot's count and suppresses the rest of the destruction while the count stays positive, as `_Ios_base_dtor` does, and the slots of destroyed streams keep their pointer and stay reserved for that stream, as `stdstr` does. The streams themselves are destroyed by the title's own code as it registers them through `__cxa_atexit`; when the last reference closes, the unsupported stream storage and callback cleanup is still the throw it was and the locale is released as for a non-standard stream. - [std::_Pad, _Thrd_join, _Thrd_detach, _Thrd_id](../../../core/libs/prx/libc/src/Threads.cpp) (libc) follow Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. `_Thrd_id` throws because whether the console returns `pthread_t` or a kernel thread ID and at what width is unknown; `_Launch` omits `_Cnd_do_broadcast_at_thread_exit` and calls `pthread_create` registered by libkernel with default attributes. The 32-byte pad layout (`_Go()` vtable at 0x0, `_Cnd_t` at 0x8, `_Mtx_t` at 0x10, `bool _Started` at 0x18) is assumed from VC12, unverified. Failing primitives or thread start throw the guest `std::system_error`; null arguments throw. `_Thrd_join` returns `_Thrd_error` (4) on `pthread_join` failure and truncates the exit code to `int`; `_Thrd_detach` returns `_Thrd_error` (4) on `pthread_detach` failure and throws when none is registered +- [siginterrupt](../../../core/libs/prx/libc/src/Process.cpp) (libc) runs FreeBSD 11.4's `siginterrupt.c` sequence through the `sigaction` and the `_sigintr` set that libkernel registers at load: it reads the signal's disposition, sets or clears the signal's bit and `SA_RESTART`, and installs the disposition again. The set is written outside libkernel's registration lock, as FreeBSD's libc writes it without one; a call before libkernel has registered throws. diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index b820c745ad..51c3a42eae 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -207,7 +207,7 @@ - [pause, sigsuspend, sigwait, sigwaitinfo](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) take the pending set of `sigtimedwait` instead of throwing. `pause` waits for a pending signal that is not blocked and whose disposition can be delivered, runs its handler on the calling thread and returns -1 with `EINTR`; an ignored signal is discarded and the wait continues, and it never returns for a signal whose handler cannot run. A signal whose registered handler has `SA_SIGINFO` still throws, and one whose disposition is the default action and that has no host equivalent (every signal except 2, 4, 6, 8, 11 and 15) is neither delivered nor discarded: it stays pending until a wait takes it, where FreeBSD runs the default action, which terminates the process for most of them. A pending `SIGALRM` with the default disposition exits the process with status 142 once it is unblocked, as FreeBSD's default action does. PPSA68091, PPSA99169, PPSA99420 and PPSA97358 import them and fail to load without them. PPSA68091 calls `pause` at one site (0x8F7266), in a loop that waits while a global holds another thread's `errno` address. PPSA99169 only stores the address of `pause` in a table of symbol addresses (0x30DB1). PPSA99420 calls `pause` at 0x3BD80F6 and 0x3C9E220, `sigwait` at 0x3C9E4C7 and `sigwaitinfo` at 0x3C9E588. PPSA97358 calls `sigwait` from QEMU's `sigwait_compat` thread ([compatfd.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/compatfd.c#L28-L53)), which `qemu_signal_init` ([main-loop.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/main-loop.c#L110-L147)) starts for `SIGIO`, `SIGALRM` and `SIGBUS` when the main loop is created, so it reaches the wait at start-up, and from the vCPU thread of the dummy accelerator ([dummy-cpus.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/accel/dummy-cpus.c#L48-L56)). `sigsuspend` swaps the process-wide mask of `_sigprocmask` for its argument, waits as `pause` does through the same helper, then restores the mask through `_sigprocmask`, and returns -1 with `EINTR`; a null set throws, as the console faults in libthr instead of returning `EFAULT`. All four words of the argument replace the mask and are restored afterwards, as in `_sigprocmask`. Java 25 LAN / DNS (PPSA99869, #5501) imports it and fails to load without it, after `sysctlnametomib`, `fegetenv` and `mincore`. None of the five waits is a cancellation point - [getsid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns the calling process ID for pid 0 and the calling process, and `ESRCH` for other process IDs, assuming the single guest process is its own session leader. PPSA74126 imports it and fails to load without it - [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating -- [signal](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records its handler with `SA_RESTART` unless the signal is in the exported `_sigintr` set, as FreeBSD's libc does (`lib/libc/gen/signal.c`). `siginterrupt`, which fills that set, is a libSceLibcInternal export and is not provided: libc does not link libkernel and cannot reach the dispositions, so only a guest that writes the set itself clears `SA_RESTART` this way. +- [signal](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records its handler with `SA_RESTART` unless the signal is in the exported `_sigintr` set, as FreeBSD's libc does (`lib/libc/gen/signal.c`). `siginterrupt`, which fills that set, is a libSceLibcInternal export that libc serves through the `sigaction` and the set libkernel registers at load (see libc). - [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console - [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console From 630daf82ec3f7f3be218e0268f26d3c7353b379a Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:04:46 -0300 Subject: [PATCH 029/428] test(libc): cover siginterrupt state boundaries --- core/libs/prx/libkernel/CMakeLists.txt | 10 +++ core/libs/tests/GuestSignalInterrupt.cpp | 110 +++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 core/libs/tests/GuestSignalInterrupt.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index d025918ba0..a10639a1b2 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -927,6 +927,16 @@ if(BUILD_TESTING) target_link_libraries(guest_signal_tests PRIVATE libkernel libc) configure_windows_unwind(guest_signal_tests) add_test(NAME guest_signals COMMAND guest_signal_tests) + add_test_executable(guest_signal_interrupt_tests ${LIBS_INCLUDE_DIR}/tests/GuestSignalInterrupt.cpp) + target_include_directories(guest_signal_interrupt_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_signal_interrupt_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_signal_interrupt_tests) + add_test(NAME guest_signal_interrupt COMMAND guest_signal_interrupt_tests) + set_tests_properties(guest_signal_interrupt PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_signal_interrupt PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() if(WIN32) set_tests_properties(guest_signals PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") diff --git a/core/libs/tests/GuestSignalInterrupt.cpp b/core/libs/tests/GuestSignalInterrupt.cpp new file mode 100644 index 0000000000..dd0370d7c8 --- /dev/null +++ b/core/libs/tests/GuestSignalInterrupt.cpp @@ -0,0 +1,110 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include + +using Handler = void (APS5_VABI *)(int); + +struct GuestSignalSet { + std::uint32_t bits[4]; +}; + +struct GuestSigaction { + std::uintptr_t handler; + int flags; + GuestSignalSet mask; +}; + +static_assert(sizeof(GuestSignalSet) == 16 && alignof(GuestSignalSet) == 4); +static_assert(sizeof(GuestSigaction) == 32 && offsetof(GuestSigaction, flags) == 8 && offsetof(GuestSigaction, mask) == 12); + +extern "C" { +extern GuestSignalSet _sigintr_nid_postfix; +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI siginterrupt_nid_postfix(int, int); +int APS5_VABI sigaction_nid_postfix(int, const GuestSigaction*, GuestSigaction*); +Handler APS5_VABI signal_nid_postfix(int, Handler); +} + +namespace { + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +bool Same(const GuestSigaction& first, const GuestSigaction& second) { + return first.handler == second.handler && first.flags == second.flags && + std::memcmp(&first.mask, &second.mask, sizeof(first.mask)) == 0; +} + +void APS5_VABI HandlerFunction(int) {} + +} + +int main() { + const GuestSignalSet initial = _sigintr_nid_postfix; + Require(initial.bits[0] == 0 && initial.bits[1] == 0 && initial.bits[2] == 0 && initial.bits[3] == 0, "initial interrupt set"); + for (int guest : {1, 32, 33, 64, 65, 96, 97, 128}) { + GuestSigaction original{}; + Require(sigaction_nid_postfix(guest, nullptr, &original) == 0, "read original disposition"); + GuestSigaction custom{reinterpret_cast(HandlerFunction), 0x42, + {{0x11111111, 0x22222222, 0x33333333, 0x44444444}}}; + Require(sigaction_nid_postfix(guest, &custom, nullptr) == 0, "install custom disposition"); + Require(sigaction_nid_postfix(guest, nullptr, &custom) == 0, "read installed disposition"); + const auto word = static_cast(guest - 1) / 32; + const auto bit = std::uint32_t{1} << (static_cast(guest - 1) % 32); + GuestSignalSet expected{{0x55555555, 0xaaaaaaaa, 0x55555555, 0xaaaaaaaa}}; + expected.bits[word] &= ~bit; + _sigintr_nid_postfix = expected; + for (int flag : {1, -1, std::numeric_limits::min(), std::numeric_limits::max()}) { + *__error_nid_postfix() = 12345; + Require(siginterrupt_nid_postfix(guest, flag) == 0, "nonzero flag"); + Require(*__error_nid_postfix() == 12345, "successful errno preservation"); + expected.bits[word] |= bit; + Require(std::memcmp(&_sigintr_nid_postfix, &expected, sizeof(expected)) == 0, "set bit without changing neighboring bits"); + GuestSigaction actual{}; + Require(sigaction_nid_postfix(guest, nullptr, &actual) == 0, "read interrupted disposition"); + auto interrupted = custom; + interrupted.flags &= ~0x2; + Require(Same(actual, interrupted), "preserve handler, mask and other flags"); + } + for (int repeat = 0; repeat < 2; ++repeat) { + *__error_nid_postfix() = 23456; + Require(siginterrupt_nid_postfix(guest, 0) == 0, "clear interrupt bit"); + Require(*__error_nid_postfix() == 23456, "zero flag errno preservation"); + expected.bits[word] &= ~bit; + Require(std::memcmp(&_sigintr_nid_postfix, &expected, sizeof(expected)) == 0, "clear bit without changing neighboring bits"); + GuestSigaction actual{}; + Require(sigaction_nid_postfix(guest, nullptr, &actual) == 0 && Same(actual, custom), "restore restart without changing disposition"); + } + Require(siginterrupt_nid_postfix(guest, -1) == 0, "make later signal interruptible"); + Require(signal_nid_postfix(guest, HandlerFunction) == HandlerFunction, "subsequent signal preserves previous handler"); + GuestSigaction actual{}; + Require(sigaction_nid_postfix(guest, nullptr, &actual) == 0 && actual.flags == 0, "signal consults shared interrupt set"); + Require(siginterrupt_nid_postfix(guest, 0) == 0, "make later signal restartable"); + Require(signal_nid_postfix(guest, HandlerFunction) == HandlerFunction, "repeat signal registration"); + Require(sigaction_nid_postfix(guest, nullptr, &actual) == 0 && actual.flags == 0x2, "signal restores restart"); + Require(sigaction_nid_postfix(guest, &original, nullptr) == 0, "restore original disposition"); + _sigintr_nid_postfix = initial; + } + const GuestSignalSet sentinels{{0x12345678, 0xabcdef01, 0x23456789, 0xbcdef012}}; + _sigintr_nid_postfix = sentinels; + GuestSigaction before{}; + Require(sigaction_nid_postfix(30, nullptr, &before) == 0, "read invalid-call control"); + for (int guest : {0, -1, 129, std::numeric_limits::min(), std::numeric_limits::max()}) { + for (int flag : {0, 1, -1}) { + *__error_nid_postfix() = 34567; + Require(siginterrupt_nid_postfix(guest, flag) == -1 && *__error_nid_postfix() == 22, "invalid signal reports EINVAL"); + Require(std::memcmp(&_sigintr_nid_postfix, &sentinels, sizeof(sentinels)) == 0, "invalid signal leaves all words unchanged"); + } + } + GuestSigaction after{}; + Require(sigaction_nid_postfix(30, nullptr, &after) == 0 && Same(before, after), "invalid calls preserve dispositions"); + _sigintr_nid_postfix = initial; +} From 0070aeafce7d01c3df3c431dae06a11420f0c319 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:13:15 +0300 Subject: [PATCH 030/428] fix(agc): keep default prebuild pool thread count at 1 and skip cached prebuilds --- .../libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp | 2 +- .../Execution/src/Driver/Shaders/ShaderRegistry.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp index 06eb91faee..aa82e33312 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -399,7 +399,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, parked.resolvedAt = std::chrono::steady_clock::now(); ++groupStats.resolved; if (cached) ++groupStats.resolvedHits; - if (compiledResult != nullptr) { + if (!cached && compiledResult != nullptr) { SubmitLibraryPrebuild([localDevice, compiledResult, address] { try { localDevice->PrebuildComputePipeline(*compiledResult, address); } catch (...) {} }); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index a478375483..b69bccd8fe 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1124,7 +1124,7 @@ public: static auto* pool = [] { const char* text = std::getenv("APS5_PREBUILD_THREADS"); std::lock_guard lifecycle(Lifecycle()); - auto* created = new RegistrationPreparePool(Closing() ? 0u : text != nullptr ? std::max(1u, static_cast(std::strtoul(text, nullptr, 10))) : std::max(2u, std::thread::hardware_concurrency() / 2u)); + auto* created = new RegistrationPreparePool(Closing() ? 0u : text != nullptr ? std::max(1u, static_cast(std::strtoul(text, nullptr, 10))) : 1u); PrebuildSlot().store(created); return created; }(); From 428cd4e3eef547dfb03842e7529f43ad44f6cd8a Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Wed, 21 Oct 2026 15:32:32 +0200 Subject: [PATCH 031/428] feat(libSceNgs2.native): parse 8-, 24- and 32-bit PCM and float waveforms --- .../libs/prx/libSceNgs2.native/src/Atrac9.cpp | 25 ++++++++++--- .../prx/libSceNgs2.native/tests/Ngs2Parse.cpp | 36 +++++++++++++++---- docs/dev/debt/libSceNgs2.native.md | 1 + 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp index 6c08a090c1..716eb61cbf 100644 --- a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp @@ -16,8 +16,10 @@ #include "Ngs2Internal.hpp" static constexpr std::uint16_t WAVE_FORMAT_PCM = 0x1; +static constexpr std::uint16_t WAVE_FORMAT_IEEE_FLOAT = 0x3; static constexpr std::uint16_t WAVE_FORMAT_EXTENSIBLE = 0xfffe; static constexpr std::uint8_t ATRAC9_GUID[16] = {0xd2, 0x42, 0xe1, 0x47, 0xba, 0x36, 0x8d, 0x4d, 0x88, 0xfc, 0x61, 0x65, 0x4f, 0x8c, 0x83, 0x6c}; +static constexpr std::uint8_t PCM_GUID[16] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}; static constexpr std::size_t FMT_ATRAC9_SIZE = 52; static constexpr std::size_t FMT_GUID_OFFSET = 24; static constexpr std::size_t FMT_CONFIG_OFFSET = 44; @@ -342,19 +344,27 @@ static int ParseAtrac9(const RiffChunks& chunks, Ngs2WaveformInfo& info) { return ParseAtrac9Loop(chunks, info); } -static int ParsePcm16(const RiffChunks& chunks, std::size_t size, Ngs2WaveformInfo& info) { +static std::uint32_t PcmWaveformType(std::uint32_t bits, bool floating) { + if (floating) return bits == 32 ? 0x18 : bits == 64 ? 0x1a : 0; + return bits == 8 ? 0x11 : bits == 16 ? SCE_NGS2_WAVEFORM_TYPE_PCM_I16L : bits == 24 ? 0x14 : bits == 32 ? 0x16 : 0; +} + +static int ParsePcm(const RiffChunks& chunks, std::size_t size, Ngs2WaveformInfo& info, bool floating) { const auto* format = chunks.format; if (chunks.formatSize < 16) return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; + const std::uint32_t bits = ReadLe16(format + 14); + const std::uint32_t type = PcmWaveformType(bits, floating); + if (type == 0) return SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT; const std::uint32_t channels = ReadLe16(format + 2); const std::uint32_t sampleRate = ReadLe32(format + 4); - const std::uint32_t bytesPerFrame = channels * sizeof(std::int16_t); - if (ReadLe16(format + 14) != 16 || channels == 0 || channels > NGS2_MAX_CHANNELS || sampleRate == 0 || sampleRate > MAX_SAMPLE_RATE || ReadLe16(format + 12) != bytesPerFrame) { + const std::uint32_t bytesPerFrame = channels * (bits / 8); + if (channels == 0 || channels > NGS2_MAX_CHANNELS || sampleRate == 0 || sampleRate > MAX_SAMPLE_RATE || ReadLe16(format + 12) != bytesPerFrame) { return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; } if (chunks.sampler != nullptr && chunks.samplerSize >= 32 && ReadLe32(chunks.sampler + 28) != 0) throw std::runtime_error("NGS2: parsing looped waveforms is not implemented"); if (chunks.dataSize > size - chunks.dataOffset || chunks.dataSize % bytesPerFrame != 0) return SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA; const std::size_t dataSize = chunks.dataSize; - info.format = {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, channels, sampleRate, 0, 0, 0}; + info.format = {type, channels, sampleRate, 0, 0, 0}; info.data_offset = static_cast(chunks.dataOffset); info.data_size = static_cast(dataSize); info.num_samples = static_cast(dataSize / bytesPerFrame); @@ -377,7 +387,12 @@ static int ParseWaveform(const RiffChunks& chunks, std::size_t size, Ngs2Wavefor std::memcmp(chunks.format + FMT_GUID_OFFSET, ATRAC9_GUID, sizeof(ATRAC9_GUID)) == 0) { return ParseAtrac9(chunks, info); } - if (tag == WAVE_FORMAT_PCM) return ParsePcm16(chunks, size, info); + if (tag == WAVE_FORMAT_EXTENSIBLE && chunks.formatSize >= FMT_GUID_OFFSET + sizeof(PCM_GUID) && + std::memcmp(chunks.format + FMT_GUID_OFFSET, PCM_GUID, sizeof(PCM_GUID)) == 0) { + return ParsePcm(chunks, size, info, false); + } + if (tag == WAVE_FORMAT_PCM) return ParsePcm(chunks, size, info, false); + if (tag == WAVE_FORMAT_IEEE_FLOAT) return ParsePcm(chunks, size, info, true); throw std::runtime_error("NGS2: parsing waveform format tag " + Ngs2Hex(tag) + " is not implemented"); } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp index a2545f52e6..37ef362c57 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp @@ -22,20 +22,28 @@ static void PutTag(std::vector& out, const char* tag) { out.insert(out.end(), tag, tag + 4); } -static std::vector PcmFile(std::uint32_t channels, std::uint32_t sampleRate, std::uint32_t bits, std::uint32_t frames) { +static std::vector PcmFile(std::uint32_t channels, std::uint32_t sampleRate, std::uint32_t bits, std::uint32_t frames, std::uint32_t tag = 1) { const std::uint32_t frameBytes = channels * bits / 8; + const std::uint32_t formatSize = tag == 0xfffe ? 40 : 16; std::vector file; PutTag(file, "RIFF"); - Put32(file, 36 + frames * frameBytes); + Put32(file, 20 + formatSize + frames * frameBytes); PutTag(file, "WAVE"); PutTag(file, "fmt "); - Put32(file, 16); - Put16(file, 1); + Put32(file, formatSize); + Put16(file, tag); Put16(file, channels); Put32(file, sampleRate); Put32(file, sampleRate * frameBytes); Put16(file, frameBytes); Put16(file, bits); + if (tag == 0xfffe) { + Put16(file, 22); + Put16(file, bits); + Put32(file, 0); + const std::uint8_t pcm[16] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}; + file.insert(file.end(), pcm, pcm + sizeof(pcm)); + } PutTag(file, "data"); Put32(file, frames * frameBytes); file.resize(file.size() + frames * frameBytes, 0x11); @@ -60,8 +68,24 @@ static void TestParsePcm() { std::memcpy(partialFrame.data() + 40, &partialSize, sizeof(partialSize)); Require(sceNgs2ParseWaveformData(partialFrame.data(), partialFrame.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA); - const auto eightBit = PcmFile(1, 44100, 8, 10); - Require(sceNgs2ParseWaveformData(eightBit.data(), eightBit.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); + struct Depth { std::uint32_t tag, bits, type; }; + for (const auto depth : {Depth{1, 8, 0x11}, Depth{1, 16, 0x12}, Depth{1, 24, 0x14}, Depth{1, 32, 0x16}, Depth{3, 32, 0x18}, Depth{3, 64, 0x1a}, + Depth{0xfffe, 8, 0x11}, Depth{0xfffe, 24, 0x14}}) { + const auto wave = PcmFile(3, 48000, depth.bits, 10, depth.tag); + const std::uint32_t frameBytes = 3 * depth.bits / 8; + const std::uint32_t dataOffset = static_cast(wave.size() - 10 * frameBytes); + Ngs2WaveformInfo parsed{}; + Require(sceNgs2ParseWaveformData(wave.data(), wave.size(), &parsed) == SCE_NGS2_OK); + Require(parsed.format.waveform_type == depth.type && parsed.format.num_channels == 3 && parsed.format.sample_rate == 48000); + Require(parsed.data_offset == dataOffset && parsed.data_size == 10 * frameBytes && parsed.num_samples == 10); + Require(parsed.audio_unit_size == frameBytes && parsed.num_audio_unit_samples == 1 && parsed.audio_frame_size == frameBytes && parsed.num_audio_frame_samples == 1); + Require(parsed.num_blocks == 1 && parsed.block[0].data_offset == dataOffset && parsed.block[0].data_size == 10 * frameBytes && parsed.block[0].num_samples == 10); + } + for (const auto depth : {Depth{1, 12, 0}, Depth{1, 64, 0}, Depth{3, 16, 0}, Depth{3, 24, 0}, Depth{0xfffe, 12, 0}}) { + const auto wave = PcmFile(1, 44100, depth.bits, 8, depth.tag); + Ngs2WaveformInfo parsed{}; + Require(sceNgs2ParseWaveformData(wave.data(), wave.size(), &parsed) == SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT); + } const auto noChannels = PcmFile(0, 44100, 16, 10); Require(sceNgs2ParseWaveformData(noChannels.data(), noChannels.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); } diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index f5a4bd1856..1a599b662b 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -29,6 +29,7 @@ - [sceNgs2VoiceQueryInfo](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - only info id 0x4001 is known: PPSA22520 passes an 8-byte buffer and branches on the first word being 1 or 2, so it is assumed to be the voice channel count (0 before the voice is set up); the second word is zeroed and other ids throw - [sceNgs2VoiceGetOwner](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - the signature `(voiceHandle, outOwnerRack, outVoiceIndex)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L439-L440) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2.cpp#L322-L326), which only logs it; neither gives an error. The errors are unmeasured: a handle that is not a live voice answers `INVALID_VOICE_HANDLE` before the outputs are checked, as in `sceNgs2RackGetVoiceHandle`, and either output pointer null answers `INVALID_OUT_ADDRESS` without writing the other - [sceNgs2VoiceControl patch](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a destination rack handle with input id 0 resolves to that rack's first voice; this matches the Dead Cells call in #4334, but selection for racks with multiple voices has not been checked against a console. Nonzero destination input ids still throw. `sceNgs2VoiceGetPortInfo` reports the resolved voice handle and zero delay. +- [NGS2 PCM waveform parsing](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - `sceNgs2ParseWaveformData` reads integer PCM (tag 1, or `WAVE_FORMAT_EXTENSIBLE` with the PCM GUID) of 8, 16, 24 and 32 bits as types 0x11, 0x12, 0x14 and 0x16, and float (tag 3) of 32 and 64 bits as 0x18 and 0x1a, and returns 0x804A8432 for other depths, after [prosper's reading of `libSceNgs2.native.sprx`](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/src/hle/audio/ngs2_waveform.cpp#L194-L223); not measured on a console. Every depth keeps the checks of the 16-bit path (channels, rate, block align, whole frames, a looped `smpl` chunk throws), which that reading does not apply. Voices still play only 16-bit PCM and ATRAC9 - [sceNgs2ParseWaveformFile](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw - [sceNgs2VoiceControl sampler filter](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown - [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. Error codes are not known for the PS5, so invalid arguments throw, including a speaker angle equal to the unit angle, on which that library loops forever. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason From d6e270ea9febfb05c56c3cca8e0e4a00bc4566c0 Mon Sep 17 00:00:00 2001 From: Evgeniy Kazarin Date: Wed, 21 Oct 2026 21:43:56 +0800 Subject: [PATCH 032/428] fix(libc): stop write-protecting resident runs on Windows A clean resident run was made read-only so that the next collect could skip GetWriteWatch, and the first guest store into it was resumed on the guest thread. The exception frame Windows builds for that fault overwrites the System V red zone below rsp (#361), which is what #5596 removed for singly mapped direct memory. Resident runs are now never armed and are collected with GetWriteWatch only. Refs #9136 Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libc/include/WindowsMappings.hpp | 49 +------------------ core/libs/tests/GuestMemory.cpp | 10 ++-- docs/dev/debt/libc.md | 1 - 3 files changed, 7 insertions(+), 53 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 624685e8a9..3db9150ba4 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -192,15 +192,11 @@ public: const auto found = views.find(base); if (found == views.end() || !writable(found->second.protection)) return false; auto& view = found->second; - const auto written = subpagesOf(base, address, address + 1); - if (view.resident && (view.armed & written) == 0) { + if (view.resident) { const auto memory = query(address); return memory.State == MEM_COMMIT && writable(memory.Protect & 0xffu); } - if (view.resident) { - resumeResidentWrites(base); - return true; - } + const auto written = subpagesOf(base, address, address + 1); invalidate(*view.page, written); protectSubpages(base, written, view.protection, "resume shared memory write", view.viewBytes); view.armed &= static_cast(~written); @@ -574,7 +570,6 @@ private: std::uintptr_t end; std::array collected; std::uint32_t fresh; - bool writeObserved = false; }; struct Window { void* view = nullptr; @@ -588,14 +583,12 @@ private: void collectResident(std::uintptr_t start, Run& run, std::uintptr_t from, std::uintptr_t stop, void** pages, std::size_t* count, bool clear, Collection collection) { const auto first = from & ~std::uintptr_t{4095}; - const bool armed = clear && run.fresh == 0 && !run.writeObserved && collection != Collection::Fresh && run.end - start >= 0x40000 && armResident(first, stop); if (run.fresh == 0 && collection != Collection::Fresh) { ULONG_PTR reported = (stop - first + 4095) / 4096; DWORD granularity = 0; if (GetWriteWatch(clear ? WRITE_WATCH_FLAG_RESET : 0, reinterpret_cast(first), stop - first, pages + *count, &reported, &granularity) != 0) fail("collect resident guest writes"); for (ULONG_PTR index = 0; index < reported; ++index) pages[*count + index] = reinterpret_cast(std::max(from, reinterpret_cast(pages[*count + index]))); *count += reported; - if (armed) rememberClean(from, stop); return; } std::vector written; @@ -633,44 +626,6 @@ private: if (collection == Collection::Fresh) flushReset(stop); } - void resumeResidentWrites(std::uintptr_t address) { - const auto run = std::prev(residentRuns.upper_bound(address)); - run->second.writeObserved = true; - forgetClean(run->first, run->second.end); - auto first = views.lower_bound(run->first); - while (first != views.end() && first->first < run->second.end) { - const auto begin = first->first; - auto end = begin; - const auto protection = first->second.protection; - auto next = first; - bool armed = false; - do { - armed = armed || next->second.armed != 0; - end = next->first + pageBytes; - ++next; - } while (next != views.end() && next->first == end && end < run->second.end && next->second.protection == protection); - if (armed) { - DWORD previous; - if (!VirtualProtect(reinterpret_cast(begin), end - begin, protection, &previous)) fail("resume resident guest writes"); - } - for (; first != next; ++first) first->second.armed = 0; - } - } - - bool armResident(std::uintptr_t from, std::uintptr_t stop) { - const auto first = views.lower_bound(from & ~(pageBytes - 1)); - if (first == views.end() || !writable(first->second.protection)) return false; - const auto protection = first->second.protection; - for (auto it = first; it != views.end() && it->first < stop; ++it) { - if (!it->second.resident || it->second.protection != protection || it->second.hostWrites != 0 || it->second.hostImports != 0 || it->second.page->pins != 0) return false; - } - DWORD previous; - const auto readOnly = protection == PAGE_EXECUTE_READWRITE ? PAGE_EXECUTE_READ : PAGE_READONLY; - if (!VirtualProtect(reinterpret_cast(from), stop - from, readOnly, &previous)) fail("arm resident memory write tracking"); - for (auto it = first; it != views.end() && it->first < stop; ++it) it->second.armed |= subpagesOf(it->first, from, stop); - return true; - } - Window open(HANDLE section, std::uint64_t offset, std::size_t bytes) { const auto lead = offset % granularity; Window window; diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index a52c5a5e2d..141f5a9e29 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -1754,7 +1754,7 @@ static void CheckSingleDirectMappingTracksWritesWithoutFaults() { #endif } -static void CheckLargeResidentWritesStopFaulting() { +static void CheckLargeResidentWritesDoNotFault() { #ifdef _WIN32 constexpr std::size_t bytes = 0x200000; std::int64_t physical = 0; @@ -1770,7 +1770,7 @@ static void CheckLargeResidentWritesStopFaulting() { Require(collect() == bytes / 4096); Require(collect() == 0 && collect() == 0); MEMORY_BASIC_INFORMATION memory{}; - Require(VirtualQuery(mapped, &memory, sizeof(memory)) == sizeof(memory) && memory.Protect == PAGE_READONLY); + Require(VirtualQuery(mapped, &memory, sizeof(memory)) == sizeof(memory) && memory.Protect == PAGE_READWRITE); auto* data = static_cast(mapped); const auto path = std::filesystem::path("anyps5-resident-read-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".bin"); @@ -1798,7 +1798,7 @@ static void CheckLargeResidentWritesStopFaulting() { Require(collect() == 1 && collect() == 0); } for (unsigned int round = 0; round < 16; ++round) Require(collect() == 0); - Require(VirtualQuery(mapped, &memory, sizeof(memory)) == sizeof(memory) && memory.Protect == PAGE_READONLY); + Require(VirtualQuery(mapped, &memory, sizeof(memory)) == sizeof(memory) && memory.Protect == PAGE_READWRITE); const auto before = g_accessViolations; const auto handler = AddVectoredExceptionHandler(1, CountAccessViolations); Require(handler != nullptr); @@ -1807,7 +1807,7 @@ static void CheckLargeResidentWritesStopFaulting() { Require(collect() == bytes / 4096 && collect() == 0); } Require(RemoveVectoredExceptionHandler(handler)); - Require(g_accessViolations == before + 1); + Require(g_accessViolations == before); Require(VirtualQuery(mapped, &memory, sizeof(memory)) == sizeof(memory) && memory.Protect == PAGE_READWRITE); Require(mprotect_nid_postfix(mapped, bytes, 1) == 0); Require(collect() == 0 && collect() == 0); @@ -3339,7 +3339,7 @@ int main(int argc, char** argv) { CheckPinnedSharedPages(); CheckFailedCollectKeepsWrites(); CheckSingleDirectMappingTracksWritesWithoutFaults(); - CheckLargeResidentWritesStopFaulting(); + CheckLargeResidentWritesDoNotFault(); CheckImportAccessDoesNotInventSharedWrites(); CheckResidentCollectPreservesPartialWrites(); CheckResidentCollectionDuringHostWrites(); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 581f589299..c4533c6d2c 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -63,7 +63,6 @@ - [__isfinitel](../../../core/libs/prx/libc/src/Math.cpp) - classifies the x87 80-bit exponent without narrowing to double or performing floating-point comparisons, following [FreeBSD msun](https://github.com/freebsd/freebsd-src/blob/main/lib/msun/src/s_isfinite.c). The signature and classification follow FreeBSD; console behaviour is unmeasured. Requires little-endian x87 long double stored in 16 bytes, as the existing guest long-double ABI does. - Windows direct-memory backing reserves a paging-file section. A [direct memory page](../../../core/libs/prx/libc/include/WindowsMappings.hpp) mapped at one guest address is private write-watched memory at that address and is not committed in the section; a page mapped at a second address moves its whole resident run into the section (committed there) and every address becomes a view of it, tracked by write protection. An unmapped resident page that is not all zeros is kept in a private reservation until it is mapped again. Committed section contents remain across unmap/remap and are released when the backing and its remaining views are destroyed. A host import alias over part of a resident run is refused. Linux retains its existing backing. Peak gameplay memory and Windows versions other than the tested host remain unverified. -- Stable Windows resident runs of at least 256 KiB use temporary read-only protection for clean write-watch proofs. The first CPU write restores the logical protection of the whole run; that run remains on native write watching until it is remapped, so repeatedly rewritten runs stay writable. Native kernel writes into guest buffers must hold [GuestArena::HostWrite](../../../core/libs/prx/libc/include/GuestArena.hpp), as shared views already require: an unbracketed WinAPI ReadFile can fail with ERROR_NOACCESS while the resident proof is armed. Guest read, pread and socket receive paths already bracket their destinations. GPU host-import access suppresses resident rearming without inventing shared-page stores. This is a host integration requirement, not a change to guest logical protection. - [getc_unlocked, putc_unlocked, getchar_unlocked, putchar_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. - [feof_unlocked and ferror_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libSceLibcInternal, implemented in libc) have no console measurement. The firmware 3.20 [export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibcInternal.c) names NIDs `NuydofHcR1w` and `yxbGzBQC5xA`. They follow FreeBSD 11 [feof.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/stdio/feof.c) and [ferror.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/stdio/ferror.c), returning 0 or 1 from the stream status without acquiring the stream lock. They inherit the host CRT limitations of the unlocked byte operations above. - [fgetws, getwc, getwchar, putwc, putwchar](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - wide stream I/O encodes and decodes wide characters as UTF-8 on the underlying byte stream; `fgetws` reads characters into the destination buffer until newline or EOF and null-terminates, returning null on stream error. Console wide stream buffering and encoding details remain unverified. From dfdfc9bca37fd3b95c8ec0980a9ccd490b788a62 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 19 Oct 2026 03:02:21 +0200 Subject: [PATCH 033/428] fix(libc): return the byte difference from strcmp, strncmp and memcmp The console's compares return the difference of the first two bytes that differ, read as unsigned char, as the prosper console oracle measured. The host functions they called return only the sign on Windows and differ by host C library. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/CMakeLists.txt | 9 +++++ core/libs/prx/libc/src/Strings.cpp | 16 +++++++-- core/libs/tests/GuestStringCompare.cpp | 49 ++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 core/libs/tests/GuestStringCompare.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index f06b05d6af..9987ba490d 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -554,6 +554,15 @@ if(BUILD_TESTING) set_tests_properties(guest_memrchr PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_string_compare_tests ${LIBS_INCLUDE_DIR}/tests/GuestStringCompare.cpp) + target_include_directories(guest_string_compare_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_string_compare_tests PRIVATE libc) + configure_windows_unwind(guest_string_compare_tests) + add_test(NAME guest_string_compare COMMAND guest_string_compare_tests) + if(WIN32) + set_tests_properties(guest_string_compare PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_daemon_tests ${LIBS_INCLUDE_DIR}/tests/GuestDaemon.cpp) target_include_directories(guest_daemon_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_daemon_tests PRIVATE libc) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 55ee19e5b1..c69f1131c7 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -20,6 +20,16 @@ int CompareUnits(char16_t left, char16_t right) { return left < right ? -1 : 1; } +int ByteDifference(const void* s1, const void* s2, size_t n, bool stopAtNul) { + const auto* left = static_cast(s1); + const auto* right = static_cast(s2); + for (size_t index = 0; index < n; ++index) { + if (left[index] != right[index]) return left[index] - right[index]; + if (stopAtNul && left[index] == 0) return 0; + } + return 0; +} + bool Contains(const char16_t* set, char16_t character) { for (; *set != 0; ++set) { if (*set == character) return true; @@ -216,7 +226,7 @@ void* APS5_VABI memmove_nid_postfix(void* dest, const void* src, size_t n) { } int APS5_VABI memcmp_nid_postfix(const void* s1, const void* s2, size_t n) { - return std::memcmp(s1, s2, n); + return std::memcmp(s1, s2, n) == 0 ? 0 : ByteDifference(s1, s2, n, false); } const void* APS5_VABI memchr_nid_postfix(const void* s, int c, size_t n) { @@ -260,11 +270,11 @@ void* APS5_VABI memmem_nid_postfix(const void* haystack, size_t haystacklen, con } int APS5_VABI strcmp_nid_postfix(const char* s1, const char* s2) { - return std::strcmp(s1, s2); + return std::strcmp(s1, s2) == 0 ? 0 : ByteDifference(s1, s2, std::numeric_limits::max(), true); } int APS5_VABI strncmp_nid_postfix(const char* s1, const char* s2, size_t n) { - return std::strncmp(s1, s2, n); + return std::strncmp(s1, s2, n) == 0 ? 0 : ByteDifference(s1, s2, n, true); } size_t APS5_VABI strlen_nid_postfix(const char* s) { diff --git a/core/libs/tests/GuestStringCompare.cpp b/core/libs/tests/GuestStringCompare.cpp new file mode 100644 index 0000000000..a30c08e959 --- /dev/null +++ b/core/libs/tests/GuestStringCompare.cpp @@ -0,0 +1,49 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI strcmp_nid_postfix(const char*, const char*); +int APS5_VABI strncmp_nid_postfix(const char*, const char*, std::size_t); +int APS5_VABI memcmp_nid_postfix(const void*, const void*, std::size_t); +} + +namespace { + +void Require(bool value) { + if (!value) std::abort(); +} + +} + +int main() { + Require(strcmp_nid_postfix("abc", "abc") == 0); + Require(strcmp_nid_postfix("abc", "abd") == -1); + Require(strcmp_nid_postfix("abd", "abc") == 1); + Require(strcmp_nid_postfix("a", "z") == -25); + Require(strcmp_nid_postfix("abc", "abcd") == -100); + Require(strcmp_nid_postfix("abcd", "abc") == 100); + Require(strcmp_nid_postfix("\x80", "a") == 31); + Require(strcmp_nid_postfix("", "") == 0); + + Require(strncmp_nid_postfix("abcdef", "abcxyz", 3) == 0); + Require(strncmp_nid_postfix("abcdef", "abcxyz", 4) == -20); + Require(strncmp_nid_postfix("abc", "xyz", 0) == 0); + Require(strncmp_nid_postfix("abc\0x", "abc\0y", 6) == 0); + Require(strncmp_nid_postfix("ab", "abc", 8) == -99); + + const unsigned char base[] = {0x01, 0x02, 0x03}; + const unsigned char higher[] = {0x01, 0x02, 0x04}; + const unsigned char top[] = {0x01, 0x02, 0xff}; + const unsigned char low[] = {0x01, 0x02, 0x01}; + Require(memcmp_nid_postfix(base, base, 3) == 0); + Require(memcmp_nid_postfix(base, higher, 3) == -1); + Require(memcmp_nid_postfix(top, low, 3) == 254); + Require(memcmp_nid_postfix(top, low, 2) == 0); + const unsigned char sign[] = {0x80}; + const unsigned char one[] = {0x01}; + Require(memcmp_nid_postfix(sign, one, 1) == 127); + const unsigned char withNul[] = {0x00, 0x05}; + const unsigned char withNulLow[] = {0x00, 0x02}; + Require(memcmp_nid_postfix(withNul, withNulLow, 2) == 3); +} From 4e57df0082b330d7dafacd374408c9b22c947211 Mon Sep 17 00:00:00 2001 From: hypermist Date: Thu, 22 Oct 2026 03:12:01 +1300 Subject: [PATCH 034/428] fix(libkernel_sys): return what the firmware returns from 20 calls it does not implement Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- .../prx/libkernel_sys/Export.Discovery.cpp | 52 ++++++------------- 1 file changed, 16 insertions(+), 36 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 8b3da583e4..0aae82f03e 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -514,13 +514,11 @@ int APS5_VABI pthread_suspend_user_context_np_nid_postfix(void) { } int APS5_VABI pthread_switch_add_np_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return 45; } int APS5_VABI pthread_switch_delete_np_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return 45; } int APS5_VABI pthread_timedjoin_np_nid_postfix(void) { @@ -599,7 +597,6 @@ int APS5_VABI sceKernelAddProcessToCanvasMap(void) { } int APS5_VABI sceKernelAddResource(void) { - NotImplemented_nid_no_patch(__func__); return 0; } @@ -679,13 +676,11 @@ int APS5_VABI sceKernelClearBootReqNotifyCount(void) { } int APS5_VABI sceKernelClearGameDirectMemory(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x8002004e); } int APS5_VABI sceKernelCloseEport(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80020001); } int APS5_VABI sceKernelCloseEventFlag(void) { @@ -704,8 +699,7 @@ int APS5_VABI sceKernelCreateBudget(void) { } int APS5_VABI sceKernelCreateEport(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80020001); } int APS5_VABI sceKernelDebugAcquireAndUpdateDebugRegister(void) { @@ -784,8 +778,7 @@ int APS5_VABI sceKernelDeleteCpumodeEvent(void) { } int APS5_VABI sceKernelDeleteEport(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80020001); } int APS5_VABI sceKernelDeleteGpuExceptionEvent(void) { @@ -829,7 +822,6 @@ int APS5_VABI sceKernelEndAppMount2(void) { } int APS5_VABI sceKernelEventLogInit(void) { - NotImplemented_nid_no_patch(__func__); return 0; } @@ -1369,8 +1361,7 @@ int APS5_VABI sceKernelGiveDirectMemoryToMapper(void) { } int APS5_VABI sceKernelHasNeoMode(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return 1; } int APS5_VABI sceKernelHwGetBdDriveInfo(void) { @@ -1409,8 +1400,7 @@ int APS5_VABI sceKernelIccGetBootTimestamp(void) { } int APS5_VABI sceKernelIccGetCPMode(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x8002002d); } int APS5_VABI sceKernelIccGetCountTime(void) { @@ -1504,8 +1494,7 @@ int APS5_VABI sceKernelIccSetBuzzer(void) { } int APS5_VABI sceKernelIccSetCPMode(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x8002002d); } int APS5_VABI sceKernelIccSetCpuInfoBit(void) { @@ -1559,8 +1548,7 @@ int APS5_VABI sceKernelIsAllowedToSelectDvdRegion(void) { } int APS5_VABI sceKernelIsAuthenticNeo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return 1; } int APS5_VABI sceKernelIsCEX(void) { @@ -1594,13 +1582,11 @@ int APS5_VABI sceKernelIsGenuineDevKit(void) { } int APS5_VABI sceKernelIsGenuineKratosCex(void) { - NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceKernelIsGenuineN(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return 1; } int APS5_VABI sceKernelIsGenuineTestKit(void) { @@ -1719,8 +1705,7 @@ int APS5_VABI sceKernelMapToolMemory(void) { } int APS5_VABI sceKernelMapTraceMemory(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x8002004e); } int APS5_VABI sceKernelMapperGetParam(void) { @@ -1769,8 +1754,7 @@ int APS5_VABI sceKernelNotifySystemSuspendStart(void) { } int APS5_VABI sceKernelOpenEport(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80020001); } int APS5_VABI sceKernelOpenInternal(void) { @@ -1814,8 +1798,7 @@ int APS5_VABI sceKernelQueryToolMemory(void) { } int APS5_VABI sceKernelQueryTraceMemory(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80020002); } int APS5_VABI sceKernelRandomizedPath(void) { @@ -1839,7 +1822,6 @@ int APS5_VABI sceKernelReleaseToolMemory(void) { } int APS5_VABI sceKernelReleaseTraceDirectMemory(void) { - NotImplemented_nid_no_patch(__func__); return 0; } @@ -2159,13 +2141,11 @@ int APS5_VABI sceKernelTitleWorkdaroundIsEnabled(void) { } int APS5_VABI sceKernelTraceMemoryTypeProtect(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x8002004e); } int APS5_VABI sceKernelTriggerEport(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80020001); } int APS5_VABI sceKernelVirtualQueryAll(void) { From 5aa33fe27144afeef820c3a7c11fa13619986e3d Mon Sep 17 00:00:00 2001 From: Nixitov Date: Mon, 19 Oct 2026 22:23:39 +0200 Subject: [PATCH 035/428] feat(libScePosixForWebKit): export pthread_setname_np AI-assisted: yes --- .../prx/libScePosixForWebKit/CMakeLists.txt | 10 ++++ core/libs/prx/libScePosixForWebKit/Export.cpp | 14 ++++++ core/libs/tests/GuestPosixThreadName.cpp | 50 +++++++++++++++++++ docs/dev/debt/libScePosixForWebKit.md | 1 + 4 files changed, 75 insertions(+) create mode 100644 core/libs/tests/GuestPosixThreadName.cpp diff --git a/core/libs/prx/libScePosixForWebKit/CMakeLists.txt b/core/libs/prx/libScePosixForWebKit/CMakeLists.txt index 036eb0577d..59d7ed5e61 100644 --- a/core/libs/prx/libScePosixForWebKit/CMakeLists.txt +++ b/core/libs/prx/libScePosixForWebKit/CMakeLists.txt @@ -45,4 +45,14 @@ if(BUILD_TESTING) set_tests_properties(guest_service_database PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_posix_thread_name_tests ${LIBS_INCLUDE_DIR}/tests/GuestPosixThreadName.cpp) + target_include_directories(guest_posix_thread_name_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_posix_thread_name_tests PRIVATE libScePosixForWebKit libkernel libc) + configure_windows_unwind(guest_posix_thread_name_tests) + add_test(NAME guest_posix_thread_name COMMAND guest_posix_thread_name_tests) + set_tests_properties(guest_posix_thread_name PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_posix_thread_name PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libScePosixForWebKit/Export.cpp b/core/libs/prx/libScePosixForWebKit/Export.cpp index ca47a2ecf7..bb2afacda3 100644 --- a/core/libs/prx/libScePosixForWebKit/Export.cpp +++ b/core/libs/prx/libScePosixForWebKit/Export.cpp @@ -1,14 +1,20 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/HostEntropy.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" #include #include #include #include +#include #include namespace { +constexpr int ThreadNameInvalid = 22; +constexpr int ThreadNameTooLong = 63; +constexpr std::size_t ThreadNameCapacity = 32; + constexpr int FnmNomatch = 1; constexpr int FnmNoescape = 0x01; constexpr int FnmPathname = 0x02; @@ -278,4 +284,12 @@ int APS5_VABI scePosixForWebKit_sysinfo() { return 0; } +int APS5_VABI pthread_setname_np_nid_postfix(Pthread thread, const char* name) { + if (thread == nullptr || name == nullptr) return ThreadNameInvalid; + if (std::strlen(name) >= ThreadNameCapacity) return ThreadNameTooLong; + std::lock_guard lock(thread->nameLock); + thread->name = name; + return 0; +} + } diff --git a/core/libs/tests/GuestPosixThreadName.cpp b/core/libs/tests/GuestPosixThreadName.cpp new file mode 100644 index 0000000000..51c0691368 --- /dev/null +++ b/core/libs/tests/GuestPosixThreadName.cpp @@ -0,0 +1,50 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include + +extern "C" { +Pthread APS5_VABI pthread_self_nid_postfix(void); +int APS5_VABI pthread_getname_np_nid_postfix(Pthread thread, char* name); +int APS5_VABI pthread_setname_np_nid_postfix(Pthread thread, const char* name); +} + +static constexpr int GUEST_EINVAL = 22; +static constexpr int GUEST_ENAMETOOLONG = 63; + +static void Require(bool value) { if (!value) std::abort(); } + +static std::string ReadName(Pthread thread) { + std::array buffer{}; + buffer.fill(0x55); + Require(pthread_getname_np_nid_postfix(thread, buffer.data()) == 0); + return std::string(buffer.data()); +} + +static void RequireStored(Pthread thread, const std::string& name) { + Require(ReadName(thread) == name); +} + +int main() { + const Pthread self = pthread_self_nid_postfix(); + RequireStored(self, ""); + + Require(pthread_setname_np_nid_postfix(self, "webkit") == 0); + RequireStored(self, "webkit"); + + Require(pthread_setname_np_nid_postfix(self, "") == 0); + RequireStored(self, ""); + + Require(pthread_setname_np_nid_postfix(self, nullptr) == GUEST_EINVAL); + Require(pthread_setname_np_nid_postfix(nullptr, "name") == GUEST_EINVAL); + + const std::string longest(31, 'b'); + Require(pthread_setname_np_nid_postfix(self, longest.c_str()) == 0); + RequireStored(self, longest); + + const std::string tooLong(32, 'c'); + Require(pthread_setname_np_nid_postfix(self, tooLong.c_str()) == GUEST_ENAMETOOLONG); + RequireStored(self, longest); + return 0; +} diff --git a/docs/dev/debt/libScePosixForWebKit.md b/docs/dev/debt/libScePosixForWebKit.md index 681a24a3d5..43f60601d3 100644 --- a/docs/dev/debt/libScePosixForWebKit.md +++ b/docs/dev/debt/libScePosixForWebKit.md @@ -9,5 +9,6 @@ - [mkstemp and isatty](../../../core/libs/prx/libScePosixForWebKit/Filesystem.cpp) (libScePosixForWebKit) follow FreeBSD's [temporary-file API](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/stdio/mktemp.c) and [terminal check](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/gen/isatty.c), without console verification. `mkstemp` replaces all trailing `X` characters, accepts a fixed name, and creates files through the guest path resolver with exclusive read/write access and mode 0600. Windows file permissions follow the existing kernel file implementation. `isatty` tests Windows console handles rather than all character devices; files, pipes and guest sockets return `ENOTTY`, closed descriptors return `EBADF`. - [arc4random](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) returns four bytes of host OS entropy (`getentropy`, `rand_s` on Windows), as `arc4random_buf` does, uniform over the 32-bit range like FreeBSD 11.4's RC4 keystream in [arc4random.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/arc4random.c). The console's generator and its reseeding are unknown; `arc4random_uniform`, `arc4random_stir` and `arc4random_addrandom` are not in the module's export set and are absent +- [pthread_setname_np](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) stores the guest thread name in libkernel's thread object, so `pthread_getname_np` returns it, and follows libkernel's `pthread_rename_np`: `EINVAL` (22) for a null thread or name, `ENAMETOOLONG` (63) from 32 bytes on, `0` otherwise, with the name truncated to 31 bytes when read back. It does not apply the Windows thread description or the job affinity that `scePthreadRename` sets, because those helpers are private to libkernel; the console's exact error numbers for this module are unverified - [fnmatch](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) follows FreeBSD 11 [fnmatch.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/gen/fnmatch.c) in the C locale: each byte is one character, `FNM_CASEFOLD` folds ASCII letters only, and bracket ranges compare byte values. `[:class:]` names are not character classes: as in FreeBSD 11 through 14, `[[:digit:]]` is the set of `[`, `:`, `d`, `i`, `g`, `t` followed by a literal `]` (classes were added to `fnmatch.c` after 14). Multibyte characters and locale collation are not modelled. A null pattern or string throws - [getservbyname](../../../core/libs/prx/libScePosixForWebKit/Resolver.cpp) (libScePosixForWebKit) searches a built-in copy of 37 lines of FreeBSD 11.4's [etc/services](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/etc/services) (ftp-data, ftp, ssh, telnet, smtp, domain, tftp, http, pop3, ntp, imap, https, rtsp, submission, imaps, pop3s with their aliases and protocols, in file order) the way [getservent.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/net/getservent.c) searches the file: case-sensitive name or alias, a null protocol takes the first match, the port in network byte order, the result in per-thread storage. The console's `/etc/services` is unknown, so any other service returns null without setting `errno`, as for a name the file lacks. `getaddrinfo` still resolves service names through the host's service database From 13982e0c7192b9571af18c4b7b847b754592f78a Mon Sep 17 00:00:00 2001 From: felek Date: Tue, 20 Oct 2026 18:33:36 +0200 Subject: [PATCH 036/428] feat(shader): decode MIMG opcodes 0xc8 and 0xd8 as image_gather4_c_a and image_gather4_c_o_a Death Stranding 2 (PPSA02015) registers shaders with both and stops on 0xc8 ("unsupported MIMG opcode 0xc8"). LLVM's gfx1030 disassembler reads the same words with bit 0 clear as image_gather4_c and image_gather4_c_o, and the _a rows already in the table are the base opcode plus 0x80 with the adjust flag, so both reuse the existing compare gathers with the adjust flag set. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BywPXoZACVi8ALV4JKvuPZ --- core/libs/tests/ImageSampleGather.cpp | 17 +++++++++++++++++ .../RdnaDecoder/src/RdnaImageOpDecoder.cpp | 2 ++ 2 files changed, 19 insertions(+) diff --git a/core/libs/tests/ImageSampleGather.cpp b/core/libs/tests/ImageSampleGather.cpp index 3d3b40d608..2182e4d5cd 100644 --- a/core/libs/tests/ImageSampleGather.cpp +++ b/core/libs/tests/ImageSampleGather.cpp @@ -62,6 +62,21 @@ static void CheckPckRejectsZeroMask(std::uint32_t encoding) { } Require(rejected); } +static void CheckGatherCompareAdjust(std::uint32_t word0, std::uint32_t opcodeId, RdnaOpcode opcode, std::uint32_t flags, std::uint32_t addressComponents) { + const std::array code{word0, 0x00801e33u}; + const RdnaInstruction instruction = DecodeRdnaMimg(0u, code, 0u); + Require(instruction.op == opcode); + Require(instruction.imageOpcodeId == opcodeId); + Require(instruction.imageSampleFlags == flags); + Require(instruction.imageAddressComponents == addressComponents); + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + TranslationContext context(program, block, 256); + context.TranslateInstruction(instruction); + Require(program.Resources().memoryInfo.size() == 1u); + Require(program.Resources().memoryInfo.back().imageSampleFlags == flags); +} static void CheckUnsupportedOpcodeDiagnostic(std::uint32_t encoding, std::string_view expected) { const std::array code{(0x3cu << 26u) | ((encoding & 0x7fu) << 18u) | (encoding >> 7u), 0u}; try { @@ -142,6 +157,8 @@ int main() { CheckPck(0x63u, RdnaOpcode::ImageGather8hPck, 0xFu, 4u); CheckPckRejectsZeroMask(0x62u); CheckPckRejectsZeroMask(0x63u); + CheckGatherCompareAdjust(0xf1200109u, 0xc8u, RdnaOpcode::ImageGather4C, RdnaImageSampleFlagCompare | RdnaImageSampleFlagAdjust, 3u); + CheckGatherCompareAdjust(0xf1600109u, 0xd8u, RdnaOpcode::ImageGather4CO, RdnaImageSampleFlagCompare | RdnaImageSampleFlagOffset | RdnaImageSampleFlagAdjust, 4u); CheckUnsupportedOpcodeDiagnostic(0x06u, "MIMG opcode 0x06"); CheckUnsupportedOpcodeDiagnostic(0xffu, "MIMG opcode 0xff"); } diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp index e3be618395..e5a71be9c7 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp @@ -86,6 +86,8 @@ constexpr ImageOpcodeInfo imageOpcodes[] = { {0x4fu, RdnaOpcode::ImageGather4CLz, nullptr, RdnaImageSampleFlagCompare | RdnaImageSampleFlagLevelZero, false, true, false}, {0x57u, RdnaOpcode::ImageGather4LzO, nullptr, RdnaImageSampleFlagLevelZero | RdnaImageSampleFlagOffset, false, true, false}, {0x58u, RdnaOpcode::ImageGather4CO, nullptr, RdnaImageSampleFlagCompare | RdnaImageSampleFlagOffset, false, true, false}, + {0xc8u, RdnaOpcode::ImageGather4C, "image_gather4_c_a", RdnaImageSampleFlagCompare | RdnaImageSampleFlagAdjust, false, true, false}, + {0xd8u, RdnaOpcode::ImageGather4CO, "image_gather4_c_o_a", RdnaImageSampleFlagCompare | RdnaImageSampleFlagOffset | RdnaImageSampleFlagAdjust, false, true, false}, {0x5fu, RdnaOpcode::ImageGather4CLzO, nullptr, RdnaImageSampleFlagCompare | RdnaImageSampleFlagLevelZero | RdnaImageSampleFlagOffset, false, true, false}, {0x61u, RdnaOpcode::ImageGather4h, nullptr, RdnaImageSampleFlagGatherHorizontal, false, true, false}, {0x62u, RdnaOpcode::ImageGather4hPck, nullptr, RdnaImageSampleFlagGatherHorizontal, false, true, false}, From a1ce9acb35015083e56e43ae3ea9fd3a4e14e3d5 Mon Sep 17 00:00:00 2001 From: felek Date: Tue, 20 Oct 2026 19:20:57 +0200 Subject: [PATCH 037/428] test(shader): check that the _a compare gathers canonicalize S# dword 3 image_gather4_c_a and image_gather4_c_o_a compile when the S# dword 3 holds only a ballot in its reserved bits, like image_sample_a, while image_gather4_c and image_gather4_c_o with the same S# still refuse it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BywPXoZACVi8ALV4JKvuPZ --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index a754f42271..3d85170f2c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -6087,6 +6087,12 @@ void verifyUnnormalizedSamplers() { const auto adjusted = recompile(reservedDword3(0xf0800f09u), userData(normalized)); require(proven(adjusted, false), "sampler dword 3: an image_sample_a whose S# dword 3 holds only a ballot in its reserved bits was not compiled"); expectFailure([&] { static_cast(recompile(reservedDword3(0xf0800f08u), userData(normalized))); }, "GetSamplerResource dword 3 is not a valid runtime value", "sampler dword 3: an image_sample whose S# dword 3 holds a ballot was accepted"); + for (const auto& [adjust, plain, compiled, accepted] : { + std::tuple{0xf1200109u, 0xf1200108u, "sampler dword 3: an image_gather4_c_a whose S# dword 3 holds only a ballot in its reserved bits was not compiled", "sampler dword 3: an image_gather4_c with a computed S# dword 3 was accepted"}, + std::tuple{0xf1600109u, 0xf1600108u, "sampler dword 3: an image_gather4_c_o_a whose S# dword 3 holds only a ballot in its reserved bits was not compiled", "sampler dword 3: an image_gather4_c_o with a computed S# dword 3 was accepted"}}) { + require(proven(recompile(reservedDword3(adjust), imageData(normalized, Type2D, Format32Float, 0u)), false), compiled); + expectFailure([&] { static_cast(recompile(reservedDword3(plain), imageData(normalized, Type2D, Format32Float, 0u))); }, "GetSamplerResource dword 3 is not a valid runtime value", accepted); + } reject(0xf09c0f18u, imageData(unnormalized, TypeCube, Format8888UNorm, 5u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a cube T# was accepted"); reject(0xf09c0f10u, imageData(unnormalized, Type3D, Format8888UNorm, 3u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a 3D T# was accepted"); reject(0xf09c0f28u, imageData(unnormalized, Type2DArray, Format8888UNorm, 5u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a 2D-array sample was accepted"); From c443b512c25202e51ee62a73031634e87b3797a5 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:33:58 +0000 Subject: [PATCH 038/428] docs(agc): describe label trace packet coverage --- docs/dev/ENVIRONMENT.md | 2 +- docs/dev/TRACING.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 7d6f4d537b..1a62dee16e 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -60,7 +60,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | Name | Kind | Source | Default | Description | |------|------|--------|---------|-------------| -| `APS5_TRACE_LABEL` | integer | `core/libs/prx/libSceAgc/Cb/src/Sync.cpp` | 0 | hex guest address of a RELEASE_MEM label whose packet is logged when recorded | +| `APS5_TRACE_LABEL` | integer | `core/libs/prx/libSceAgc/Cb/src/Sync.cpp`, `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/LabelHistory.cpp` | 0 | hex guest address; logs a matching RELEASE_MEM when recorded, and nearby RELEASE_MEM, WRITE_DATA, COPY_DATA, DMA_DATA and WAIT_REG_MEM packets when executed | ### libSceAgcDriver diff --git a/docs/dev/TRACING.md b/docs/dev/TRACING.md index 8cb84bb4a4..ba11ed362f 100644 --- a/docs/dev/TRACING.md +++ b/docs/dev/TRACING.md @@ -45,7 +45,7 @@ Set a switch before starting the emulator. Unless noted, any value enables it. M | `APS5_TRACE_HOST_IMPORT` | Host-import mapping attempts, memory-map details, failures and retirement. | | `APS5_TRACE_IMAGE_TABLE` | Image-table resource materialization and descriptor selection. | | `APS5_TRACE_INDIRECT_DRAWS` | Indirect draw packets that pass the draw precheck. | -| `APS5_TRACE_LABEL` | `RELEASE_MEM` packets that target the address in this hexadecimal value. | +| `APS5_TRACE_LABEL` | A matching `RELEASE_MEM` when recorded, and nearby `RELEASE_MEM`, `WRITE_DATA`, `COPY_DATA`, `DMA_DATA` and `WAIT_REG_MEM` packets when executed. Set the value to the guest address in hexadecimal. | | `APS5_TRACE_LATE_LABELS` | GPU label writes observed after their waiting submission was handled. | | `APS5_TRACE_QUERY` | Every sixteenth guest-memory page query, including cost and caller address. | | `APS5_TRACE_RESOURCE_KEY_MISSES` | Resource-content key changes that cause draw or dispatch resource-cache misses. | From 06ca8ffb7fd05a787f1b471ba04039e445b2a318 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:53:39 +0200 Subject: [PATCH 039/428] docs(debt): record the storage image refusal over uncommitted guest memory StorageTexture throws when none of an image's guest pages is committed. Three titles reach it intermittently in runs of a few minutes, on AMD and NVIDIA, and the log names no address. The entry states the gap and that the console behavior for such a descriptor is not known. Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceAgcDriver.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index c4ea080793..cacb31d7a5 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -106,6 +106,7 @@ - [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. Explicit-size file writes (`write`, `pwrite`, `writev`, `pwritev`, AIO and `fwrite`) synchronize their source ranges through the [host-read hook](../../../core/libs/prx/libc/src/GuestArena.cpp); other host readers are not covered. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default. - [`APS5_SKIP_REFUSED_DRAWS`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp) (libSceAgcDriver) drops a draw whose graphics stage the recompiler refuses instead of failing the queue, so the frame lacks that draw, and the stage is compiled and refused again at each such draw. Compute refusals still fail. - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending saves its bytes through the unwatch hook, which also runs when a failed collect takes a range out of the watch. A range that leaves the watch without the hook running over it (an import fallback that unwatched without saving) leaves its pending results without saved bytes; storing such results throws. A unit whose results were stored while its blocks were watched keeps only a generation; when its blocks leave the watch while nothing is pending, the hook saves nothing, and results that land in it afterwards have neither saved bytes nor a watch, so storing them throws too. +- [Storage images over uncommitted guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) - a storage image whose guest range has no committed page when it is created throws in the `StorageTexture` constructor, which aborts the guest. Three titles reach this intermittently in runs of a few minutes, on AMD and NVIDIA alike; in two of the three logs, lines about storage images over released or rewritten memory (`memory no longer registered`, `guest memory changed while GPU results were pending`) come shortly before it. The log does not name the image's address. What the console does with a storage descriptor over memory that is not committed is unknown, and which allocation the guest recreates there was not found. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory when the import probe keeps the write watch and a settle queue exists: those of 1 MiB or more stay watched, and smaller ones return to the watch once their settle has completed, unless a storage image overlaps them (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. - [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. On Windows, the NVIDIA driver (617.42) marks every page of a host import of private `MEM_WRITE_WATCH` memory written, asynchronously, until a GPU use of the import completes; a 4-byte read of it on the settle queue ends that even while main-queue work that used the import first is still running, and an earlier import of the same pages whose first use runs after that read marks none of them (`agc_import_settle_first_use`, `agc_import_settle_earlier_import`). Such an import stays watched only after a 4-byte read of it on the settle queue has completed inside the window. Each one waits for that read, which keeps its window open for one round trip on the settle queue, so a same-value CPU store from another thread in that time counts as unchanged too. An import made where no settle queue exists is compared instead. An import of under 1 MiB is not settled inside its window, because the settle blocks the importing thread and a title that imports a 64 KiB buffer around every submission lost about 19 % of its frame rate to that wait (Dripl1x, #6060). It is compared until its read, submitted in a batch by a later import lookup under `GpuMutex` while the imports match the registry, has completed; a later lookup then returns its whole blocks to the write watch, each stamped as a CPU store, so whatever was derived from them while they were compared reads as changed once. Its blocks stay compared while storage results are pending over them, and those of an import retired before that stay compared. - [Settled dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a dma-buf import of 1 MiB or more is read by a 4-byte copy on the settle queue right after it is made (`settleImport`), so the kernel maps its pages for the GPU there and not in the first submission that uses it (about 2 s for a 3 GiB import on RADV). The import's buffer is shared with the settle queue's family. An import made at map time settles on the guest thread that maps the range, inside its map call, so that thread waits while the kernel maps the pages: 0.47 to 2.2 s for the 3 GiB range of Hades (PPSA03355) and 0.49 to 4.3 s for the 4.5 GiB range of ASTRO BOT (PPSA21567) on RADV. Both titles map these ranges at start-up; a title that maps large ranges during gameplay stalls its mapping thread for as long. An import made from a submission settles under the GPU mutex, so its frame still pays for it; a smaller import, and every import on a device with no settle queue (one without a copy-capable queue besides the main one), pays in its first submission. A range imported through a host pointer, as lavapipe imports shared memory, is not settled From d324106dc7a6efebdfb1fac111f9148137c765ab Mon Sep 17 00:00:00 2001 From: Kraken <10617703+UnhealthyKraken@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:28:12 +0100 Subject: [PATCH 040/428] docs(libkernel): record cleanup unwind and allocation limits --- docs/dev/debt/libkernel.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 381e509889..e86e2ce375 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -74,6 +74,7 @@ - [Fixed anonymous mmap](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `MAP_PRIVATE | MAP_ANON | MAP_FIXED` replaces the requested range through the existing guest mapping machinery and zeroes the new memory. `MAP_EXCL` is checked under the allocation-registry mutation lock, including virtual reservations, and returns `ENOMEM` without changing an occupied range; without `MAP_FIXED` it is `EINVAL`. Lengths are rounded to 16 KiB and fixed addresses must be aligned. The complete rounded range must lie inside the guest arena or, for POSIX fixed anonymous `mmap` on Windows, one current or registered native thread stack; other mapping APIs do not opt into stack replacement. Native stack replacement keeps the reservation, decommits and recommits fresh pages, and rejects foreign allocations, ranges spanning reservations and host `PAGE_GUARD` pages before mutation. Current-stack ranges must end at least 64 KiB below the active host frame; callers replacing another thread's stack must coordinate with that thread. If recommitting fails, the call returns `ENOMEM` with the range still reserved; thread cleanup attempts to recommit it before restoring write access. This follows FreeBSD 11.4 [argument validation and rounding](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L226-L299), [fixed replacement](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_map.c#L1455-L1464), and [collision errno](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L1604-L1613). The console behaviour has not been measured. AnyPS5 starts the guest arena at 0x200000000, so the fixed path rejects the low 0x400000 address requested by PICO-8 PPSA99808. The console address-space layout is unmeasured; executable placement outside the host arena is tracked in #5217. Static catalog disassembly finds `0x1012` in PPSA98001/PPSA99808 and `0x5012` in PPSA99808; these titles were not run. - [Mapping hints with no room above them](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - a mapping without `MAP_FIXED` whose hint lies above the guest address space, or has no free range at or above it, is placed from the bottom of the guest arena, as FreeBSD 11.4's [`vm_mmap_object`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L1569) retries through [`vm_map_find_min`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_map.c#L1555-L1584) from its minimum address. FreeBSD's minimum is the end of the largest possible data segment rather than the start of the arena, and the console's address-space layout is not measured. PS5CEMU-HAR (PPSA99360) gives `sceKernelReserveVirtualRange` a hint near its own code, which is a host address here because the relinked executable lies outside the guest arena, so its JIT gets memory more than 2 GiB from its code and does not start - [_sceKernelRtldThreadAtexitIncrement, _sceKernelRtldThreadAtexitDecrement](../../../core/libs/prx/libkernel/System/src/Rtld.cpp) (libkernel) - the argument is the `dso` address a title's `libc.prx` passes to `__cxa_thread_atexit` (PPSA14632), which fails when Increment returns nonzero. As in the [fpPS4](https://github.com/red-prig/fpPS4) reimplementation, Increment takes a reference on the loaded image containing the address and Decrement releases one; the variants without the leading underscore are assumed to match +- [pthread_cleanup_push, pthread_cleanup_pop](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel, NIDs `4ZeZWcMsAV0`, `RVxb0Ssa5t0`) share the caller-record cleanup list with `__pthread_cleanup_push_imp` and `__pthread_cleanup_pop_imp`, following [FreeBSD 11.4 thr_clean.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libthr/thread/thr_clean.c). The firmware body reading reported by [PR8428](https://github.com/boykopovar/AnyPS5/pull/8428) also sets the thread's unwind-disabled flag in the heap form; that flag is not modeled here, and runtime-created guest threads skip guest-frame unwinding in these exit paths. The firmware observation is attributed to that contributor, not independently reproduced. Heap records use C++ `new` here, whereas the FreeBSD reference leaves the cleanup list unchanged if `malloc` fails; console allocation-failure behavior and its interaction with unwinding remain unverified. - [tcgetattr, tcsetattr](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) - every file and pipe descriptor is taken as not a terminal (`ENOTTY`), and a socket gets `EADDRNOTAVAIL` for `AF_INET` and `EOPNOTSUPP` otherwise, as FreeBSD's `in_control` and `pru_control_notsupp` answer a termios ioctl; what descriptors 0 to 2 of a title are on the console, and whether any of them is a terminal there, is unknown - [cfsetspeed](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel, NID `C1MQ6OaXRjo`) - console behavior and module/library versions are unverified. The prototype, uint32 speed and 44-byte termios layout (input speed at 36, output speed at 40) follow the public PS5 payload SDK [termios.h](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/termios.h) and [sys/_termios.h](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/sys/_termios.h). The module association follows its libkernel stub table and the NID rehashes to the aerolib name. Both speed fields accept every uint32 value and no flags or control characters change, as [FreeBSD 11.4 termios.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/gen/termios.c) does. No terminal I/O is performed. Null pointers throw where the reference dereferences them. - [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - descriptor 0 is never reported readable and descriptors 1 and 2 are always writable, as for a console title with no input; reads from descriptor 0 still go to the host stdin. `/dev/random` and `/dev/urandom` follow `randomdev_poll` for a seeded device under [`selscan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1178-L1236), which asks once with the flags of every set the descriptor is in: in the read set they are readable and reported in no other set, and otherwise they are reported in the write and exception sets they were asked in. A regular file, a directory or a file read from app0.zar is always readable and writable and never in the exception set, as `vop_stdpoll` answers, on every host. Guest sockets and libSceNet sockets are polled. A kqueue is readable while one of its events is triggered and is never writable or in the exception set, as FreeBSD 11.4's [`kqueue_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c#L1900-L1924) answers, on every host. Other descriptors are polled through the host on Linux, while on Windows pipes are checked every millisecond with the readiness test `poll` uses and any other descriptor throws. Dungeons (PPSA16064) selects on descriptor 0, and RetroArch (PPSA99169) selects on its web server's wake-up pipe next to its listening socket From 0597011b106f6b8d3d66b33a4b8a054a34de4a4c Mon Sep 17 00:00:00 2001 From: thuanquang Date: Thu, 22 Oct 2026 02:19:57 +0700 Subject: [PATCH 041/428] docs(libSceNet): correct when sceNetAccept reads the inherited nonblocking mode Co-Authored-By: Claude --- docs/dev/debt/libSceNet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index fa35cc1780..32d836ac16 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -179,7 +179,7 @@ ### Functional - [sceNetResolverStartAton6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reverse-resolves a 16-byte IPv6 address through the host resolver (`getnameinfo` with `NI_NAMEREQD`) into the caller's buffer of `len` bytes, as `sceNetResolverStartAton` does for IPv4. The `(rid, addr, hostname, len, timeout, retry, flags)` signature comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L992-L997). An address with no name, or a name longer than `len - 1` bytes, fails with `RESOLVER_ENODNS` (`0x804101E1`) and leaves the buffer untouched; whether the console truncates instead is not verified. Abort flag 2 (ATON preservation) fails the next call with `EINTR` as it does `sceNetResolverStartAton`. Timeout and retry are ignored, and any nonzero flags throw because their meaning is unknown -- [sceNetAccept](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) inherits the listener's non-blocking mode through the existing `SO_NBIO` setter, as the libkernel socket path does and FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L571) copies the socket state. The mode is sampled when accept starts; changes between connection creation and accept are not modelled. Not measured on a console. +- [sceNetAccept](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) inherits the listener's non-blocking mode through the existing `SO_NBIO` setter, as the libkernel socket path does and FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L571) copies the socket state. The mode is read after the host accept returns; changes between connection creation and accept are not modelled. Not measured on a console. - [sceNetConnect to AF_UNSPEC](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. Windows sockets stay non-blocking and the library waits for readiness until the stored timeout. A stream receive that times out still reports `ETIMEDOUT` rather than FreeBSD's `EAGAIN`; a datagram receive reports `EAGAIN`. Blocking stream sends return the queued byte count on timeout, or `EAGAIN` when no bytes were queued. From b19b1c817ec599ac334a60061f5b6ba4dc7278cc Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Wed, 21 Oct 2026 21:21:03 +0200 Subject: [PATCH 042/428] fix(libSceAvPlayer): refuse sceAvPlayerSetTrickSpeed without a source and sceAvPlayerSetAvailableBandwidth after one --- core/libs/prx/libSceAvPlayer/Export.cpp | 2 +- core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp | 1 + core/libs/prx/libSceAvPlayer/src/Player.cpp | 10 ++++++++-- core/libs/tests/GuestAvPlayer.cpp | 3 +++ docs/dev/debt/libSceAvPlayer.md | 2 +- 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAvPlayer/Export.cpp b/core/libs/prx/libSceAvPlayer/Export.cpp index 2cdb89887d..fcd67b2e16 100644 --- a/core/libs/prx/libSceAvPlayer/Export.cpp +++ b/core/libs/prx/libSceAvPlayer/Export.cpp @@ -151,7 +151,7 @@ int APS5_VABI sceAvPlayerSetAvailableBandwidth(AvPlayerInternal* h, uint32_t sta (void)minimum_bandwidth; (void)maximum_bandwidth; if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return SCE_OK; + return ToPlayer(h)->SetAvailableBandwidth(); } int APS5_VABI sceAvPlayerSetLogCallback(void* callback, void* user_data) { diff --git a/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp b/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp index 49bf029391..4ff8e6de39 100644 --- a/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp +++ b/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp @@ -110,6 +110,7 @@ public: int JumpToTime(std::uint64_t milliseconds); int SetLooping(bool enabled); int SetTrickSpeed(std::int32_t trickSpeed); + int SetAvailableBandwidth(); int SetAvSyncMode(std::uint32_t mode); bool GetVideoData(AvPlayerFrameInfoEx& info); bool GetVideoData(AvPlayerFrameInfo& info); diff --git a/core/libs/prx/libSceAvPlayer/src/Player.cpp b/core/libs/prx/libSceAvPlayer/src/Player.cpp index 191a185847..bc6eb44066 100644 --- a/core/libs/prx/libSceAvPlayer/src/Player.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Player.cpp @@ -334,14 +334,20 @@ int Player::SetLooping(bool enabled) { } int Player::SetTrickSpeed(std::int32_t trickSpeed) { - if (trickSpeed == 0) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; std::lock_guard lock(mutex); + if (!source) return SCE_AVPLAYER_ERROR_OPERATION_FAILED; + if (trickSpeed == 0) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; if (state == State::Stop || state == State::EndOfFile || state == State::Error) return SCE_AVPLAYER_ERROR_OPERATION_FAILED; speed = trickSpeed; - if (source) source->SetSpeed(trickSpeed); + source->SetSpeed(trickSpeed); return SCE_OK; } +int Player::SetAvailableBandwidth() { + std::lock_guard lock(mutex); + return source ? SCE_AVPLAYER_ERROR_OPERATION_FAILED : SCE_OK; +} + int Player::SetAvSyncMode(std::uint32_t mode) { if (mode != SyncModeDefault && mode != SyncModeNone) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; std::lock_guard lock(mutex); diff --git a/core/libs/tests/GuestAvPlayer.cpp b/core/libs/tests/GuestAvPlayer.cpp index 68ef734c2f..eb9d2e150d 100644 --- a/core/libs/tests/GuestAvPlayer.cpp +++ b/core/libs/tests/GuestAvPlayer.cpp @@ -833,6 +833,8 @@ void TestPlayback() { Check(sceAvPlayerPostInit(player, &post) == 0, "post init failed"); Check(sceAvPlayerPostInit(player, nullptr) == InvalidParams, "post init accepted null data"); Check(sceAvPlayerStreamCount(player) == OperationFailed, "stream count without a source"); + Check(sceAvPlayerSetTrickSpeed(player, 100) == OperationFailed, "trick speed accepted without a source"); + Check(sceAvPlayerSetTrickSpeed(player, 0) == OperationFailed, "trick speed checked the speed before the source"); Check(!sceAvPlayerIsActive(player), "active without a source"); Check(sceAvPlayerAddSource(player, "/app0/missing.mp4") == OperationFailed, "missing file accepted"); Check(sceAvPlayerAddSource(player, "/app0/missing/../avplayer.mp4") == OperationFailed, "missing parent accepted"); @@ -1068,6 +1070,7 @@ void TestExtendedExports() { Check(sceAvPlayerSetAvailableBandwidth(player, 4000, 1000, 8000) == 0, "bandwidth rejected"); Check(sceAvPlayerSetAvailableBandwidth(nullptr, 4000, 1000, 8000) == InvalidParams, "bandwidth accepted for a null player"); Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "add source failed"); + Check(sceAvPlayerSetAvailableBandwidth(player, 4000, 1000, 8000) == OperationFailed, "bandwidth accepted after a source"); Check(WaitFor([&] { return events.Seen(EventReady); }), "ready event missing"); CheckStreamInfoEx(player); Check(sceAvPlayerEnableStream(player, VideoStream) == 0, "enable video failed"); diff --git a/docs/dev/debt/libSceAvPlayer.md b/docs/dev/debt/libSceAvPlayer.md index 5a2f1941ed..2e7cd27eee 100644 --- a/docs/dev/debt/libSceAvPlayer.md +++ b/docs/dev/debt/libSceAvPlayer.md @@ -2,7 +2,7 @@ ### Silent stubs -- [libSceAvPlayer](../../../core/libs/prx/libSceAvPlayer/Export.cpp): `sceAvPlayerSetLogCallback` accepts a callback that is never called, as the player produces no log messages, and `sceAvPlayerSetAvailableBandwidth` has no effect, as it governs HLS sources, which `sceAvPlayerAddSource` does not implement. +- [libSceAvPlayer](../../../core/libs/prx/libSceAvPlayer/Export.cpp): `sceAvPlayerSetLogCallback` accepts a callback that is never called, as the player produces no log messages, and `sceAvPlayerSetAvailableBandwidth` has no effect, as it governs HLS sources, which `sceAvPlayerAddSource` does not implement; after a source is added it returns `OPERATION_FAILED`, as [prosper's reading of the module](https://github.com/mattias800/prosper/blob/8dcb5dfdb9997ede7c76783995010d6abd1222f8/prosper/src/hle/video/avplayer.cpp#L960-L972) describes. - [sceAvPlayerSetTrickSpeed](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) with a negative speed runs the clock backwards but delivers no frames; when a forward speed is set again, playback resumes from the rewound time. ### Unknown function info From 7d6c475b957d53288e1e29dda2187e19569a258d Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:18:28 +0000 Subject: [PATCH 043/428] fix(agc): an address-based build evicts idle imports instead of copying past the cap AcquireRegistered imports every registered GPU range whole without eviction, so a range the import budget refuses and no mirror serves is copied on every build, and copies past APS5_ADDRESS_COPY_MAX_MIB (64 MiB by default) abort the process. Resident Evil Village (PPSA01556) aborts this way on a 768 MiB heap 8 MiB over the budget. The pass now retries such a range's import with eviction when, and only when, copying it would take the build's copies past the cap, that is when the build would abort otherwise. Imports the build already looked up or made count as held (buildFloor) and are never evicted; the retire moves the import epoch, which Upload and the address-space cache already honour, and a retired import lives through the hold. Every other build still imports without eviction, so the set of imported ranges stays as stable across builds as before. A rate-limited [gpu] line reports such an import. The copy reason of a heap now names what refused it (an import over the host import budget, a heap not fully write-watched, a refused heap mirror) instead of "outside the write-watched arena" for every heap. --- .../Graphics/src/GuestBufferMemory.cpp | 51 ++++++++++++++++--- docs/dev/debt/libSceAgcDriver.md | 2 +- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 074681e607..32de7adb49 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -277,6 +277,7 @@ struct HostImports { bool allocationFailed = false; std::uint8_t reportNoted = 0; std::map spaceUses; + std::uint64_t budgetRefusals = 0; }; HostImports& Imports() { @@ -1584,6 +1585,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s ++counts.refused; counts.refusedBytes += lease[index]->bytes; ++counts.remembered; + ++state.budgetRefusals; return nullptr; } auto run = importRun(lease, address, largest, budget); @@ -1683,6 +1685,7 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import if (evict) state.evictingRefusals.insert_or_assign(std::pair{base, bytes}, EvictingRefusal{base, bytes, GuestAllocations::Mapped(run.ranges.begin(), run.ranges.end()), state.epoch, state.liveBytes, budget, room, importWindow.load(std::memory_order_relaxed), state.buildFloor, ready, readyHeld, joined}); if (stepDown && containing != run.ranges.end() && room >= (*containing)->bytes) return importAllocation(context, state, address, lease, requester, end, evict, std::min(bytes / 2, room)); if (containing != run.ranges.end() && run.ranges.size() > 1) remember((*containing)->address, (*containing)->bytes, GuestAllocations::Mapped{*containing}); + ++state.budgetRefusals; return nullptr; } for (auto it = first; it != state.imports.end() && it->first < base + bytes;) { @@ -2502,6 +2505,11 @@ bool importsNow(const Context& context, HostImports& state, const GuestAllocatio return context.hostImportAlignment != 0 && (importsStale(context, state) || importAllocation(context, state, address, lease, ImportRequester::Build, 0, false) != nullptr); } +const char* heapCopyReason(bool overBudget, bool watched) { + if (overBudget) return watched ? "import over the host import budget, heap mirror refused" : "import over the host import budget, heap not fully write-watched"; + return watched ? "heap mirror refused" : "heap not fully write-watched"; +} + // The mirror for a leased image range: the existing one, or a new one when none exists or the // registered Range object changed (a protection change). Null when the range cannot be mirrored. An // existing writable mirror's blocks are appended to `blocks` for the caller's one compare of every @@ -3634,22 +3642,32 @@ void GuestBufferMemory::AcquireRegistered() { // The import lock is taken per range: a mirror's preparation may wait for recorded work, which // must not happen under it. regions.reserve(lease.size()); + static const std::uint64_t copyLimit = [] { + const char* value = std::getenv("APS5_ADDRESS_COPY_MAX_MIB"); + return (value ? std::strtoull(value, nullptr, 10) : 64ull) << 20u; + }(); + const auto limit = addressCopyLimitOverride.load(std::memory_order_relaxed); + const auto cap = limit != 0 ? limit : copyLimit; std::vector copies; std::vector imported; const HostImport* covering = nullptr; std::uint64_t coveredBegin = 0; std::uint64_t coveredEnd = 0; + std::uint64_t copiedBytes = 0; for (const auto& range : lease) { if (!range->readable || !range->gpu) continue; validate(range->address, range->bytes); Region region{range->address, range->address + range->bytes, range->writable, {}, nullptr}; const HostImport* entry = nullptr; + bool overBudget = false; if (importable && covering != nullptr && range->address >= coveredBegin && range->address + range->bytes <= coveredEnd) { entry = covering; } else if (importable) { auto& state = Imports(); std::lock_guard lock(state.mutex); + const auto refusals = state.budgetRefusals; entry = importAllocation(context, state, range->address, lease, ImportRequester::Build, 0, false); + overBudget = state.budgetRefusals != refusals; covering = entry; if (entry != nullptr) { coveredBegin = entry->base; @@ -3687,17 +3705,36 @@ void GuestBufferMemory::AcquireRegistered() { committed = 0; for (const auto& [first, last] : copied.backed) committed += last - first; } - const char* reason = !mirrorsEnabled() ? "mirrors disabled by APS5_NO_LEASE_MIRROR" : copied.sparse ? "unreadable pages" : !range->releasable ? "image mirror refused" : "outside the write-watched arena"; + if (overBudget && copiedBytes + committed > cap) { + auto& state = Imports(); + std::lock_guard lock(state.mutex); + entry = importAllocation(context, state, range->address, lease, ImportRequester::Build, 0, true); + if (entry != nullptr) { + covering = entry; + coveredBegin = entry->base; + coveredEnd = entry->base + entry->bytes; + } + static auto lastReport = std::chrono::steady_clock::now() - std::chrono::seconds(60); + if (entry != nullptr && std::chrono::steady_clock::now() - lastReport > std::chrono::seconds(10)) { + lastReport = std::chrono::steady_clock::now(); + std::fprintf(stderr, "[gpu] host import budget: 0x%llx+0x%llx imported by evicting idle imports, as copying it would pass APS5_ADDRESS_COPY_MAX_MIB\n", static_cast(range->address), static_cast(range->bytes)); + } + } + if (entry != nullptr) { + regions.pop_back(); + region.hostBacked = true; + region.direct = entry; + regions.push_back(std::move(region)); + imported.push_back(range->address); + continue; + } + copiedBytes += committed; + const char* reason = !mirrorsEnabled() ? "mirrors disabled by APS5_NO_LEASE_MIRROR" : copied.sparse ? "unreadable pages" : !range->releasable ? "image mirror refused" : heapCopyReason(overBudget, GuestMemory::Watched(range->address, range->bytes)); copies.push_back({copied.begin, copied.end, committed, reason}); } releaseImportedMirrors(context, imported); regionsSorted = sortedLookup; - static const std::uint64_t copyLimit = [] { - const char* value = std::getenv("APS5_ADDRESS_COPY_MAX_MIB"); - return (value ? std::strtoull(value, nullptr, 10) : 64ull) << 20u; - }(); - const auto limit = addressCopyLimitOverride.load(std::memory_order_relaxed); - if (const auto overflow = AddressCopyOverflow(std::move(copies), limit != 0 ? limit : copyLimit); !overflow.empty()) { + if (const auto overflow = AddressCopyOverflow(std::move(copies), cap); !overflow.empty()) { std::fprintf(stderr, "FATAL: %s\n", overflow.c_str()); std::fflush(stderr); std::abort(); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 8b92e54ef7..a77e8bd64f 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -209,7 +209,7 @@ - [16-bit depth with stencil](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is stored as `D24_UNORM_S8_UINT`, or `D32_SFLOAT_S8_UINT`, on a device without `D16_UNORM_S8_UINT` as an attachment. Depth comparisons, clears and the depth image see the wider format's precision, so results at the 16-bit quantization boundary can differ from the console; the constant depth bias is scaled by 256 for D24, and a non-zero constant on the D32 float fallback throws. Not measured on console hardware. - [Depth surfaces created over storage writes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth surface created without HTILE copies its depth plane from the storage image at the plane's address, a pending one first, when that image has the plane's extent, one level, a 2D view or a 2D array view (its layer 0 is copied) and a 16-bit format for a 16-bit plane or a 32-bit format for a 32-bit plane. A multisampled surface, a surface the device stores in another format, a surface with HTILE and a surface without such an image start from `DB_DEPTH_CLEAR`, and the stencil plane from `DB_STENCIL_CLEAR`; CPU writes to the plane's memory are not read. Not measured on the console, where the plane is that memory. - [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does, but only on a queue that has never delivered an interrupt from such a release: the queue is latched the first time a release interrupt reaches the event queue, and flips on that queue deliver nothing while its own release interrupts keep arriving. The latch is cleared when the queue has flipped five times without another interrupt from its own releases: the fifth flip delivers the event and unlatched the queue, so a title that waits on the flip event after latching recovers only if it submits further flips. The bound cannot be by time: the next release interrupt can arrive seconds later than a suppressed flip across loading stalls, and delivering an unexpected flip event on a queue that pairs flips with releases can crash; no fixed window short enough to unblock a waiting title keeps such a title alive. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; nor is how the console behaves for a title that waits on the flip event and also raises release interrupts on the same queue. -- [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less the larger of 2 GiB and half that heap (imports, type-3 mirrors and buffers and textures placed in host memory share it; on a 16 GiB machine whose host heap is half the memory, a 2 GiB reserve left too little for the rest). Where the device enables `VK_EXT_memory_budget`, that host heap's reported budget less its other usage and an eighth, in 64 MiB steps, read at most once a second, also bounds the default budget once an import's `vkAllocateMemory` has failed; a set `APS5_HOST_IMPORT_MIB` is never lowered by the report. The report's other usage includes the host-visible copies and heap mirrors that stand in for refused imports, so it grows with the refusals; the first time the report would lower the budget, `[gpu] host import budget: host heap ...` prints the heap's reported budget and usage and the live imports. An import's `vkAllocateMemory` failure within an eighth of the budget, or the third in a row, lowers the budget to the live imports until the device or the configured budget changes; an isolated failure further below only marks its range failed. Live imports are not capped by `maxMemoryAllocationCount`: NVIDIA's Windows driver reports 4096, while Village held 27,167 live imports without a failure. A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A heap mirror, sparse or not, whose range has left the write watch is refreshed in full at each use, every block counted as written; a build plans its range again only once the import path takes the range. A request that may not evict (an address-based build, an import when a range is mapped) remembers a refused range or block, with the live bytes and the budget at the refusal, and is answered from that memory, still counted as refused, until live imports drop below those bytes, the budget changes or one of its ranges changes. A request that may evict remembers its refusal too, answering later requests for the same run from memory with the room the refusal left (a request whose range fits in that room steps down to a smaller block without scanning the imports again, and a refusal that stepped down is remembered for each block it tried), while no import was made or retired, the budget has not grown, the import window and the run are unchanged, and no import can have become evictable since: the use count stays below the first use at which an import that was neither evictable nor held could leave the 256-lookup protection, and either the build floor has not risen or no held import could leave it yet. A join remembers its refusal the same way, apart from other requests for the same run, so a buffer bound across refused ranges does not try the join again at every upload. The shutdown line `[gpu] host imports at shutdown` gives the live, made, refused (and remembered) and failed counts per requester. The 64 MiB window, the 256-lookup protection, the 2 GiB reserve, the eighth, the 64 MiB steps and the three failures are choices, not measured. +- [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less the larger of 2 GiB and half that heap (imports, type-3 mirrors and buffers and textures placed in host memory share it; on a 16 GiB machine whose host heap is half the memory, a 2 GiB reserve left too little for the rest). Where the device enables `VK_EXT_memory_budget`, that host heap's reported budget less its other usage and an eighth, in 64 MiB steps, read at most once a second, also bounds the default budget once an import's `vkAllocateMemory` has failed; a set `APS5_HOST_IMPORT_MIB` is never lowered by the report. The report's other usage includes the host-visible copies and heap mirrors that stand in for refused imports, so it grows with the refusals; the first time the report would lower the budget, `[gpu] host import budget: host heap ...` prints the heap's reported budget and usage and the live imports. An import's `vkAllocateMemory` failure within an eighth of the budget, or the third in a row, lowers the budget to the live imports until the device or the configured budget changes; an isolated failure further below only marks its range failed. Live imports are not capped by `maxMemoryAllocationCount`: NVIDIA's Windows driver reports 4096, while Village held 27,167 live imports without a failure. A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. An address-based build does not evict either, except for a range the budget refused (or remembers refusing) and no mirror serves whose copy would take the build's copies past `APS5_ADDRESS_COPY_MAX_MIB`. A heap mirror, sparse or not, whose range has left the write watch is refreshed in full at each use, every block counted as written; a build plans its range again only once the import path takes the range. A request that may not evict (an address-based build, an import when a range is mapped) remembers a refused range or block, with the live bytes and the budget at the refusal, and is answered from that memory, still counted as refused, until live imports drop below those bytes, the budget changes or one of its ranges changes. A request that may evict remembers its refusal too, answering later requests for the same run from memory with the room the refusal left (a request whose range fits in that room steps down to a smaller block without scanning the imports again, and a refusal that stepped down is remembered for each block it tried), while no import was made or retired, the budget has not grown, the import window and the run are unchanged, and no import can have become evictable since: the use count stays below the first use at which an import that was neither evictable nor held could leave the 256-lookup protection, and either the build floor has not risen or no held import could leave it yet. A join remembers its refusal the same way, apart from other requests for the same run, so a buffer bound across refused ranges does not try the join again at every upload. The shutdown line `[gpu] host imports at shutdown` gives the live, made, refused (and remembered) and failed counts per requester. The 64 MiB window, the 256-lookup protection, the 2 GiB reserve, the eighth, the 64 MiB steps and the three failures are choices, not measured. - [Periodic pipeline cache saves skip busy intervals](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp) (libSceAgcDriver) - the saver thread snapshots the Vulkan pipeline cache only when no pipeline creation holds it (`TryLockForSnapshot`); otherwise it skips that 10 s interval. A blocking snapshot queued behind a long creation (an NVIDIA compile of a 1.5M-word compute pipeline runs for minutes) makes the writer-preferring `shared_mutex` hold every later pipeline creation on every queue until that compile ends, which froze Yōtei's (PPSA26344) loading at frame 797-799 for the whole compile. While creations overlap without a gap, no periodic save happens, so a crash in that stretch loses the pipelines built since the last save; `SaveNow` and the teardown save still wait for the lock. How often busy stretches skip saves in practice is not measured. - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. - [Resource cache entries holding released textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled texture the texture cache drops while something else holds it, or makes without caching (`admitTexture` refusing a first use), is marked released, and `ResourceCache::Insert` refuses descriptor sets that hold one and removes the entries that do. The removal waits for the next insert: until then `Find` still returns those entries and their textures stay alive. One process-wide flag starts the sweep, so while the sampled cache is over its budget every insert scans all entries (1,024 at start, up to 16,384 after growth, or `APS5_RESOURCE_CACHE_ENTRIES`, each with its textures); that cost is not measured. When `ReleaseTextureCaches` runs on device memory exhaustion, the sampled textures these entries hold are only marked released and are freed at the next insert, so the immediate allocation retry gets no memory back from them. A draw or dispatch whose textures the cache refused builds its descriptor sets again on each use for as long as the cache stays full; the count bounds (`APS5_RESOURCE_CACHE_ENTRIES`, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) still bound nothing by bytes, and sampled textures made outside the texture cache (the disabled cache, depth-surface copies) are never marked. Measured on one title on one RTX 4090: with the default bounds, sampled textures held outside the cache peaked at 2,836 MiB (7,830 MiB before) and device-local heap 0 stayed under its 22.9 GB budget for the whole run (26,357 MiB before, which aborted with `VK_ERROR_OUT_OF_DEVICE_MEMORY` at the wolf scene). Whether the extra descriptor-set builds cost frame time elsewhere is not measured. From 6fcf7b6573cc6821f517224a3413f91eaabbaf38 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:19:39 +0000 Subject: [PATCH 044/428] test(agc): an address-based build of a partly unwatched heap over the import budget agc_address_copy_cap registers a 2 MiB heap, a 1 MiB idle range and a 64 KiB busy range, imports the idle and busy ranges, and sets the import budget so the heap fits only if the idle import is evicted. One 64 KiB block of the heap is then removed from the write watch, so the heap can be neither imported without eviction nor mirrored, and its copy is past APS5_ADDRESS_COPY_MAX_MIB=1. The build must serve the heap through an import or a mirror that holds the guest bytes. Before the eviction ahead of the copy cap it copied the heap and aborted the process. agc_address_copy_cap_remembered first builds with the block already out of the write watch and the copy cap raised above the heap: the heap's import is refused and remembered, no mirror serves it, and it is copied. An unrelated registration then makes the next build plan its ranges again, under the 1 MiB cap: that build is answered from the remembered refusal and must still evict instead of copying past the cap. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 + .../libSceAgcDriver/tests/BdaResources.cpp | 131 ++++++++++++++++++ .../prx/libSceAgcDriver/tests/BdaTests.hpp | 1 + .../prx/libSceAgcDriver/tests/Graphics.cpp | 26 ++++ 4 files changed, 164 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 4a6440642d..b46f9d11f7 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2476,6 +2476,12 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test(NAME agc_address_copy_cap COMMAND agc_driver_graphics_tests --address-copy-cap) + add_test(NAME agc_address_copy_cap_remembered COMMAND agc_driver_graphics_tests --address-copy-cap-remembered) + set_tests_properties(agc_address_copy_cap agc_address_copy_cap_remembered PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_address_copy_cap agc_address_copy_cap_remembered PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_emulated_color_compare_srgb_decode COMMAND agc_driver_emulated_color_compare_tests) set_tests_properties(agc_driver_emulated_color_compare_srgb_decode PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SRGB_SHADER_DECODE=1" diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index e4ea60da83..22f222aecb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -3123,6 +3123,137 @@ void RunHeapMirrorRunUnwatchTests(const Context& context, const BdaTestAccess& a std::cout << "an address-based build refreshes the unwatched heap of a run in full and its watched neighbour by its writes\n"; } +void RunAddressCopyCapTests(const Context& context, const BdaTestAccess& access, bool remembered) { + constexpr std::size_t block = 65536; + constexpr std::size_t heapBytes = 32 * block; + constexpr std::size_t idleBytes = 16 * block; + constexpr std::size_t busyBytes = block; +#ifdef _WIN32 + if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) { + std::cout << "guest arena unavailable or not write-watched: the address copy cap not tested\n"; + return; + } +#else + if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) { + std::cout << "write watch unavailable: the address copy cap not tested\n"; + return; + } +#endif + struct Piece { + void* pointer = nullptr; + void* mapping = nullptr; + std::size_t allocated = 0; + std::size_t bytes = 0; + }; + std::array pieces{}; + const std::array, 4> sizes{{{heapBytes + block, heapBytes}, {heapBytes + block, idleBytes}, {busyBytes + block, busyBytes}, {busyBytes + block, busyBytes}}}; + for (std::size_t index = 0; index < pieces.size(); ++index) { + auto& piece = pieces[index]; + piece.allocated = sizes[index].first; + piece.bytes = sizes[index].second; +#ifdef _WIN32 + piece.pointer = GuestArena::GuestArenaAllocate_nid_postfix(piece.allocated, block); + Require(piece.pointer != nullptr, "cannot allocate the address copy cap ranges in the guest arena"); + GuestArena::GuestArenaCommit_nid_postfix(piece.pointer, piece.allocated, PAGE_READWRITE, piece.allocated); + piece.mapping = piece.pointer; +#else + piece.mapping = mmap(nullptr, piece.allocated + block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(piece.mapping != MAP_FAILED, "cannot map the address copy cap ranges"); + piece.pointer = reinterpret_cast((reinterpret_cast(piece.mapping) + block - 1) & ~std::uintptr_t{block - 1}); +#endif + std::memset(piece.pointer, 0x5a, piece.allocated); + } + if (pieces[1].pointer < pieces[0].pointer) { + std::swap(pieces[0].pointer, pieces[1].pointer); + std::swap(pieces[0].mapping, pieces[1].mapping); + } + const auto heap = reinterpret_cast(pieces[0].pointer); + const auto idle = reinterpret_cast(pieces[1].pointer); + const auto busy = reinterpret_cast(pieces[2].pointer); + Require(heap < idle, "the idle range does not follow the heap in the registry"); +#ifndef _WIN32 + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(pieces[0].pointer, heapBytes); +#endif + const auto registry = [&](bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + for (std::size_t index = 0; index < 3; ++index) { + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, pieces[index].pointer, pieces[index].bytes, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, pieces[index].pointer); + } + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + const auto unrelated = [&](bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, pieces[3].pointer, pieces[3].bytes, true, false, false); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, pieces[3].pointer); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + registry(true); +#ifdef _WIN32 + AgcDriver::GuestMemory::Unwatch(idle, idleBytes); + AgcDriver::GuestMemory::Unwatch(busy, busyBytes); +#endif + auto importing = context; + importing.hostImportAlignment = block; + SetHostImportBudget(heapBytes + idleBytes + busyBytes - block); + Require(HostImportFor(importing, idle, 64) != nullptr && HostImportFor(importing, busy, 64) != nullptr, "the idle and busy ranges were not imported"); + Require(HostImportLiveBytes() == idleBytes + busyBytes && !HostImportCovers(importing, heap, heapBytes), "the imports before the build are not the idle and busy ranges"); + Require(AgcDriver::GuestMemory::Watched(heap, heapBytes), "the heap is not write-watched"); + const auto before = MirrorCounters(); + const auto builds = [] { return HostImportCounters().requesters[static_cast(ImportRequester::Build)]; }; + const auto unwatch = [&] { + AgcDriver::GuestMemory::Unwatch(heap + heapBytes / 2, block); + Require(AgcDriver::GuestMemory::Watched(heap, heapBytes / 2) && !AgcDriver::GuestMemory::Watched(heap, heapBytes), "the heap is not partly outside the write watch"); + }; + if (remembered) { + unwatch(); + SetAddressCopyLimit(2 * heapBytes); + { + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.Upload(true); + Require(MirrorCounters().heapMirrors == before.heapMirrors && !HostImportCovers(importing, heap, heapBytes), "the first build did not copy the heap whose import the budget refused"); + leased.WriteBack(); + } + SetAddressCopyLimit(0); + unrelated(true); + } + for (int use = 0; use < 300; ++use) Require(HostImportFor(importing, busy, 64) != nullptr, "the busy range lost its import"); + if (!remembered) unwatch(); + const auto refused = builds(); + { + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.Upload(true); + const bool imported = HostImportCovers(importing, heap, heapBytes); + Require(imported || (!remembered && MirrorCounters().heapMirrors == before.heapMirrors + 1), "the heap is neither imported nor mirrored"); + const auto ranges = leased.AddressRanges(); + const auto found = std::find_if(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin == heap; }); + Require(found != ranges.end() && found->deviceAddress != 0u, "the heap is missing from the BDA table"); + Require(imported || access.addressBytes(found->deviceAddress)[heapBytes / 2 + 3] == std::byte{0x5a}, "the heap mirror does not hold the guest bytes"); + leased.WriteBack(); + } + Require(!remembered || builds().remembered == refused.remembered + 1, "the build under the copy cap was not answered from the refusal the first build remembered"); + SetHostImportBudget(0); + if (remembered) unrelated(false); + registry(false); + ClearImageMirrors(importing.device); + ClearHostImports(importing.device); + ClearSnapshotCopies(importing.device); +#ifndef _WIN32 + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(pieces[0].pointer, heapBytes); +#endif + for (const auto& piece : pieces) { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(piece.pointer, piece.allocated); + GuestArena::GuestArenaRelease_nid_postfix(piece.pointer, piece.allocated); +#else + munmap(piece.mapping, piece.allocated + block); +#endif + } + std::cout << (remembered ? "an address-based build answered from a remembered budget refusal evicts instead of copying a partly unwatched heap past the cap\n" : "an address-based build serves a partly unwatched heap whose import is over the budget\n"); +} + void hostWriteWatchExclusionTests() { #ifdef _WIN32 std::vector host(64, 0x31); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp b/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp index 6aea082fff..2834c21a1e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp @@ -20,6 +20,7 @@ bool RunSparseBufferHoleTests(const AgcDriver::Graphics::Context& context, const void RunHeapMirrorUnwatchTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access, bool throughImport); void RunSparseHeapMirrorUnwatchTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access); void RunHeapMirrorRunUnwatchTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access); +void RunAddressCopyCapTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access, bool remembered); void RunGuestAllocationTests(); void RunGuestLeaseWaitTests(); void RunUnmappedGapTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 428c1ce75e..6f57edbac2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -11000,6 +11000,32 @@ int main(int argc, char** argv) { Require(mock.live == 0, "the heap mirror run unwatch test leaked Vulkan objects"); return 0; } + if (argc == 2 && (std::string_view(argv[1]) == "--address-copy-cap" || std::string_view(argv[1]) == "--address-copy-cap-remembered")) { +#ifdef _WIN32 + _putenv_s("APS5_ADDRESS_COPY_MAX_MIB", "1"); +#else + setenv("APS5_ADDRESS_COPY_MAX_MIB", "1", 1); +#endif + { + mock = MockVulkan{}; + auto context = mockContext(); + context.bufferDeviceAddress = true; + context.limits.maxStorageBufferRange = 1u << 27; + RunAddressCopyCapTests(context, { + [](VkBuffer buffer) -> std::span { return mockBufferMemory(buffer); }, + {}, + [](VkDeviceAddress address) { + const auto offset = address - 0x100000000000ULL; + const auto buffer = reinterpret_cast(offset / 0x10000); + return mockBufferMemory(buffer).subspan(offset % 0x10000); + }, + {}, + {} + }, std::string_view(argv[1]) == "--address-copy-cap-remembered"); + } + Require(mock.live == 0, "the address copy cap test leaked Vulkan objects"); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--address-space-teardown") { addressSpaceTeardownTests(); std::cout << "address-space device teardown releases cached guest allocations\n"; From 307bf8157a97b2c20d8abbbd591377910eeb4ea1 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:04:49 +0000 Subject: [PATCH 045/428] perf(agc): name a copied heap range's reason only in the overflow report The address-based build named the copy reason of every copied releasable range on each plan, and a heap's reason asked GuestMemory::Watched, which takes the tracker's global mutex and walks its coverage and the arena's host regions, only to label a FATAL line that is rarely printed. A copy now records that it is a heap and whether the budget refused its import, and AddressCopyOverflow names the reason while it builds the report, for the copies the report lists. The report's text is unchanged. --- .../libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp | 2 ++ .../prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp | 7 ++++--- core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp | 3 +++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index fa552ac524..7b06e9ae9c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -258,6 +258,8 @@ struct AddressCopy { std::uint64_t end; std::uint64_t committed; const char* reason; + bool heap = false; + bool overBudget = false; }; std::string AddressCopyOverflow(std::vector copies, std::uint64_t limit); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 32de7adb49..995d078bc1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -2972,7 +2972,8 @@ std::string AddressCopyOverflow(std::vector copies, std::uint64_t l std::string message = text; for (std::size_t index = 0; index < copies.size() && index < 8; ++index) { const auto& copy = copies[index]; - std::snprintf(text, sizeof(text), " 0x%llx+0x%llx (%.1f MiB committed, %s)", static_cast(copy.begin), static_cast(copy.end - copy.begin), copy.committed / 1048576.0, copy.reason); + const char* reason = copy.heap ? heapCopyReason(copy.overBudget, GuestMemory::Watched(copy.begin, copy.end - copy.begin)) : copy.reason; + std::snprintf(text, sizeof(text), " 0x%llx+0x%llx (%.1f MiB committed, %s)", static_cast(copy.begin), static_cast(copy.end - copy.begin), copy.committed / 1048576.0, reason); message += text; } return message; @@ -3729,8 +3730,8 @@ void GuestBufferMemory::AcquireRegistered() { continue; } copiedBytes += committed; - const char* reason = !mirrorsEnabled() ? "mirrors disabled by APS5_NO_LEASE_MIRROR" : copied.sparse ? "unreadable pages" : !range->releasable ? "image mirror refused" : heapCopyReason(overBudget, GuestMemory::Watched(range->address, range->bytes)); - copies.push_back({copied.begin, copied.end, committed, reason}); + const char* reason = !mirrorsEnabled() ? "mirrors disabled by APS5_NO_LEASE_MIRROR" : copied.sparse ? "unreadable pages" : !range->releasable ? "image mirror refused" : nullptr; + copies.push_back({copied.begin, copied.end, committed, reason, reason == nullptr, overBudget}); } releaseImportedMirrors(context, imported); regionsSorted = sortedLookup; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 22f222aecb..2e087f8692 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -3673,5 +3673,8 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { Require(AddressCopyOverflow({{0x1000, 0x3000, 0x2000, "uncommitted pages"}}, 0x2000).empty(), "copies within the limit were refused"); const auto copies = AddressCopyOverflow({{0x1000, 0x2000, 0x1000, "not mirrored"}, {0x10000, 0x30000, 0x18000, "uncommitted pages"}}, 0x2000); Require(!copies.empty() && copies.find("0x10000+0x20000 (0.1 MiB committed, uncommitted pages)") < copies.find("0x1000+0x1000"), "the copy limit does not name the largest copy first"); + Require(!AgcDriver::GuestMemory::Watched(0x10000, 0x20000), "the unregistered heap copy range is write-watched"); + const auto heapCopies = AddressCopyOverflow({{0x10000, 0x30000, 0x18000, nullptr, true, true}, {0x40000, 0x50000, 0x10000, nullptr, true, false}}, 0x2000); + Require(heapCopies.find("0x10000+0x20000 (0.1 MiB committed, import over the host import budget, heap not fully write-watched)") != std::string::npos && heapCopies.find("0x40000+0x10000 (0.1 MiB committed, heap not fully write-watched)") != std::string::npos, "the copy limit does not name a copied heap's reason"); ClearSnapshotCopies(context.device); } From 6c1f63674e24295d089fa014491c3ef5e4e75b82 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:07:35 +0000 Subject: [PATCH 046/428] test(libc): time shared guest mappings on Windows windows_shared_map_cost maps 256, 16384 and 65536 pages of shared memory (4 MiB, 256 MiB and 1 GiB) through WindowsMappings::Map, then releases them, five times each, and prints the median map and release times, the time per page and the number of mapped views. It only measures and always passes, so a build of this commit and of a later one can be compared on the same machine. --- core/libs/CMakeLists.txt | 8 +++ core/libs/tests/WindowsSharedMapCost.cpp | 78 ++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 core/libs/tests/WindowsSharedMapCost.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index f84fa93f40..c7ef053323 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -323,6 +323,14 @@ if(BUILD_TESTING) endif() add_test(NAME windows_write_watch COMMAND windows_write_watch_tests) set_tests_properties(windows_write_watch PROPERTIES TIMEOUT 20) + add_test_executable(windows_shared_map_cost_tests tests/WindowsSharedMapCost.cpp) + target_include_directories(windows_shared_map_cost_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_compile_definitions(windows_shared_map_cost_tests PRIVATE _WIN32_WINNT=0x0A00) + if(MINGW) + target_compile_options(windows_shared_map_cost_tests PRIVATE -fasynchronous-unwind-tables) + endif() + add_test(NAME windows_shared_map_cost COMMAND windows_shared_map_cost_tests) + set_tests_properties(windows_shared_map_cost PROPERTIES TIMEOUT 120) endif() if(WIN32) diff --git a/core/libs/tests/WindowsSharedMapCost.cpp b/core/libs/tests/WindowsSharedMapCost.cpp new file mode 100644 index 0000000000..ce6c25b6e7 --- /dev/null +++ b/core/libs/tests/WindowsSharedMapCost.cpp @@ -0,0 +1,78 @@ +#define NOMINMAX +#include "prx/libc/include/WindowsMappings.hpp" +#include +#include +#include +#include +#include +#include + +namespace { +using Mappings = GuestArena::WindowsMappings; +constexpr std::size_t PageBytes = 16384; +constexpr int Rounds = 5; + +[[noreturn]] void Fail(const char* what) { + std::fprintf(stderr, "windows shared map cost: %s (error %lu)\n", what, GetLastError()); + std::exit(1); +} + +std::size_t MappedViews(const unsigned char* begin, std::size_t bytes) { + std::set bases; + for (auto cursor = begin; cursor < begin + bytes;) { + MEMORY_BASIC_INFORMATION info{}; + if (VirtualQuery(cursor, &info, sizeof(info)) != sizeof(info)) Fail("view query failed"); + if (info.Type == MEM_MAPPED) bases.insert(info.AllocationBase); + cursor = static_cast(info.BaseAddress) + info.RegionSize; + } + return bases.size(); +} + +struct Sample { + double mapMs = 0; + double releaseMs = 0; + std::size_t views = 0; +}; + +Sample MapOnce(std::size_t pages) { + const auto bytes = pages * PageBytes; + HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, static_cast(static_cast(bytes) >> 32), static_cast(bytes), nullptr); + auto* memory = static_cast(Mappings::Get().Reserve(nullptr, bytes)); + if (section == nullptr || memory == nullptr) Fail("cannot allocate shared memory"); + const auto start = std::chrono::steady_clock::now(); + Mappings::Get().Map(memory, bytes, section, 0, PAGE_READWRITE); + Sample sample; + sample.mapMs = std::chrono::duration(std::chrono::steady_clock::now() - start).count(); + sample.views = MappedViews(memory, bytes); + memory[0] = 1; + memory[bytes - 1] = 1; + const auto released = std::chrono::steady_clock::now(); + Mappings::Get().Reset(memory, bytes); + sample.releaseMs = std::chrono::duration(std::chrono::steady_clock::now() - released).count(); + VirtualFree(memory, 0, MEM_RELEASE); + CloseHandle(section); + return sample; +} + +double Median(std::vector values) { + std::sort(values.begin(), values.end()); + return values[values.size() / 2]; +} + +} + +int main() { + for (const std::size_t pages : {std::size_t{256}, std::size_t{16384}, std::size_t{65536}}) { + std::vector map; + std::vector release; + std::size_t views = 0; + for (int round = 0; round < Rounds; ++round) { + const auto sample = MapOnce(pages); + map.push_back(sample.mapMs); + release.push_back(sample.releaseMs); + views = sample.views; + } + std::printf("shared map of %zu pages (%zu MiB): map median %.2f ms (%.2f us per page), release median %.2f ms, %zu views\n", pages, pages * PageBytes >> 20, Median(map), Median(map) * 1000.0 / static_cast(pages), Median(release), views); + } + return 0; +} From 0bf6a77e8fc320827a88c0b77622f9a8397c7842 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Tue, 20 Oct 2026 14:43:27 -0400 Subject: [PATCH 047/428] fix(libScePlayGo): answer ids past the listed scenario chunks as unlisted A dump with sce_sys/playgo-scenario.json and no chunk table lists chunks 0 to 999 through scePlayGoGetChunkId, but every other id was still reported as installed. Destruction AllStars (PPSA02630) asks for chunk ids from 0 up until one is rejected; because no id ever was, the walk wrapped the 16-bit id and grew an array of 16-byte entries until a 5.18 GiB allocation failed about one second into startup ("Ran out of memory allocating 5565054976 bytes"). An id outside the listed 0 to 999 is now answered like an unlisted id of a dump without any playgo file: APS5_PLAYGO_UNLISTED_CHUNKS=installed or =absent answers it, and without an answer the call throws naming the missing chunk table. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libScePlayGo/Export.cpp | 6 +++-- core/libs/tests/GuestPlayGo.cpp | 35 ++++++++++++++++++++++++--- docs/dev/debt/libScePlayGo.md | 2 +- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libScePlayGo/Export.cpp b/core/libs/prx/libScePlayGo/Export.cpp index 9fae1d940e..a9e1ebb21e 100644 --- a/core/libs/prx/libScePlayGo/Export.cpp +++ b/core/libs/prx/libScePlayGo/Export.cpp @@ -106,13 +106,15 @@ static UnlistedChunkAnswer UnlistedChunks() { static bool IsKnownChunk(uint16_t id) { const auto& chunks = Chunks(); - if (chunks.defined) return chunks.ids.contains(id); - if (chunks.scenario || id == 0) return true; + if (chunks.ids.contains(id)) return true; + if (chunks.defined) return false; switch (UnlistedChunks()) { case UnlistedChunkAnswer::Installed: return true; case UnlistedChunkAnswer::Absent: return false; case UnlistedChunkAnswer::Stop: break; } + if (chunks.scenario) + throw std::runtime_error("scePlayGo: sce_sys/playgo-chunk.dat is missing and sce_sys/playgo-scenario.json lists no chunks, so chunk ID " + std::to_string(id) + " outside the listed ids 0 to " + std::to_string(PLAYGO_SCENARIO_CHUNK_COUNT - 1) + " cannot be checked; set APS5_PLAYGO_UNLISTED_CHUNKS=installed or =absent to answer for this dump"); throw std::runtime_error("scePlayGo: sce_sys/playgo-chunk.dat and sce_sys/playgo-scenario.json are missing, so chunk ID " + std::to_string(id) + " cannot be checked; set APS5_PLAYGO_UNLISTED_CHUNKS=installed or =absent to answer for this dump"); } diff --git a/core/libs/tests/GuestPlayGo.cpp b/core/libs/tests/GuestPlayGo.cpp index c03f4761ca..2a243f5d19 100644 --- a/core/libs/tests/GuestPlayGo.cpp +++ b/core/libs/tests/GuestPlayGo.cpp @@ -73,11 +73,15 @@ static int CheckScenarioWithoutChunkTable(const char* root) { int handle = 0; Require(scePlayGoInitialize(&init) == 0); Require(scePlayGoOpen(&handle, nullptr) == 0); - const std::uint16_t anyIds[3] = {1, 10, 0xFFFF}; + const std::uint16_t listedIds[3] = {1, 10, 999}; std::int8_t loci[3] = {0, 0, 0}; - Require(scePlayGoGetLocus(handle, anyIds, 3, loci) == 0 && loci[0] == 3 && loci[1] == 3 && loci[2] == 3); - const PlayGoToDo todo[2] = {{0, 3, 0}, {0xFFFF, 3, 0}}; + Require(scePlayGoGetLocus(handle, listedIds, 3, loci) == 0 && loci[0] == 3 && loci[1] == 3 && loci[2] == 3); + const PlayGoToDo todo[2] = {{0, 3, 0}, {999, 3, 0}}; Require(scePlayGoSetToDoList(handle, todo, 2) == 0); + for (const std::uint16_t unlisted : {std::uint16_t{1000}, std::uint16_t{0xFFFF}}) { + std::int8_t locus = 0; + RequireMissingChunkMetadata([&] { scePlayGoGetLocus(handle, &unlisted, 1, &locus); }); + } std::uint32_t chunkCount = 0; Require(scePlayGoGetChunkId(handle, nullptr, 0, &chunkCount) == 0 && chunkCount == 1000); std::vector ids(chunkCount + 1, 0xFFFF); @@ -209,6 +213,27 @@ static int CheckNoPlayGoFileAnswered(const char* root, const char* answer) { return 0; } +static int CheckScenarioWithoutChunkTableAnswered(const char* root, const char* answer) { + constexpr int badChunkId = static_cast(0x80B2000C); + SetUnlistedChunks(answer); + std::filesystem::current_path(root); + PlayGoInitParams init{}; + int handle = 0; + Require(scePlayGoInitialize(&init) == 0); + Require(scePlayGoOpen(&handle, nullptr) == 0); + const std::uint16_t unlisted[2] = {1000, 0xFFFF}; + std::int8_t loci[2] = {0, 0}; + const int result = scePlayGoGetLocus(handle, unlisted, 2, loci); + if (std::string_view(answer) == "installed") Require(result == 0 && loci[0] == 3 && loci[1] == 3); + else Require(result == badChunkId); + const std::uint16_t listed[2] = {0, 999}; + Require(scePlayGoGetLocus(handle, listed, 2, loci) == 0 && loci[0] == 3 && loci[1] == 3); + std::uint32_t chunkCount = 0; + Require(scePlayGoGetChunkId(handle, nullptr, 0, &chunkCount) == 0 && chunkCount == 1000); + Require(scePlayGoClose(handle) == 0); + return 0; +} + static int CheckUnknownUnlistedChunkAnswer(const char* root) { SetUnlistedChunks("maybe"); std::filesystem::current_path(root); @@ -260,6 +285,8 @@ int main(int argc, char** argv) { if (mode == "--no-file-unknown-answer") return CheckUnknownUnlistedChunkAnswer(argv[2]); if (mode == "--package-chunk-table" || mode == "--scenario-with-chunk-table") return CheckPackageChunkTable(argv[2]); if (mode == "--scenario-only") return CheckScenarioWithoutChunkTable(argv[2]); + if (mode == "--scenario-only-installed") return CheckScenarioWithoutChunkTableAnswered(argv[2], "installed"); + if (mode == "--scenario-only-absent") return CheckScenarioWithoutChunkTableAnswered(argv[2], "absent"); if (mode == "--ficm-only") return CheckRejectedLayout(argv[2], "playgo-ficm.dat"); if (mode == "--hash-table-only") return CheckRejectedLayout(argv[2], "playgo-hash-table.dat"); if (mode == "--invalid-chunk-table") return CheckRejectedLayout(argv[2], "playgo-chunk.dat"); @@ -272,6 +299,8 @@ int main(int argc, char** argv) { RunLayout(argv[0], "--package-chunk-table", {{"playgo-chunk.dat", ChunkTableFile("plgo")}}); RunLayout(argv[0], "--scenario-with-chunk-table", {{"playgo-chunk.dat", ChunkTableFile()}, {"playgo-scenario.json", "{}"}}); RunLayout(argv[0], "--scenario-only", {{"playgo-scenario.json", "{}"}}); + RunLayout(argv[0], "--scenario-only-installed", {{"playgo-scenario.json", "{}"}}); + RunLayout(argv[0], "--scenario-only-absent", {{"playgo-scenario.json", "{}"}}); RunLayout(argv[0], "--ficm-only", {{"playgo-ficm.dat", "x"}}); RunLayout(argv[0], "--hash-table-only", {{"playgo-hash-table.dat", "x"}}); RunLayout(argv[0], "--invalid-chunk-table", {{"playgo-chunk.dat", "not a chunk table"}}); diff --git a/docs/dev/debt/libScePlayGo.md b/docs/dev/debt/libScePlayGo.md index 40a6135747..89f271596b 100644 --- a/docs/dev/debt/libScePlayGo.md +++ b/docs/dev/debt/libScePlayGo.md @@ -4,5 +4,5 @@ - [scePlayGoRequestNextChunk, scePlayGoSnapshot](../../../core/libs/prx/libScePlayGo/Export.cpp) (libScePlayGo) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePlayGo.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim - [scePlayGoSetLanguageMask](../../../core/libs/prx/libScePlayGo/Export.cpp) (libScePlayGo) - stores the mask that `scePlayGoGetLanguageMask` then reports, and returns `SCE_PLAYGO_ERROR_BAD_HANDLE` for another handle, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/playgo/playgo.cpp#L346-L359) does on the PS4. shadPS4 also returns `SCE_PLAYGO_ERROR_NOT_INITIALIZED` before `scePlayGoInitialize` and starts from the system language's mask; here the mask starts with every language and initialization is not tracked. Not measured on a PS5 -- [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - the chunk set comes from `playgo-chunkdefs.xml` (every listed chunk plus chunks 0 through the default chunk), otherwise from `sce_sys/playgo-chunk.dat`, whose `plgx` header is read as holding the chunk count as a 16-bit value at offset 0x0A and ids 0 to count - 1 are taken as valid. The field was identified on four dumps, where exactly that many 0x20-byte entries follow from offset 0x100 (RIDE 4 PPSA01599: 35, Persona 5 Royal PPSA05110: 25, LEGO Horizon Adventures PPSA14632 and Kena PPSA01802: 1); the rest of the format is not decoded and ids are assumed to be contiguous. Every listed chunk is reported as installed (`LOCAL_FAST`). Without either file, a package with `sce_sys/playgo-scenario.json` (PPSA01748, whose dump lists no chunks) accepts every chunk id as installed, and `scePlayGoGetChunkId` lists ids 0 to 999. A title that builds its set of installed chunks from that list skips any chunk it does not see. The console's chunk limit is not known here, and a package with more chunks would see ids from 1000 up as not listed. A package with no `playgo-*` file in `sce_sys` (PPSA03001, PPSA03490 and PPSA01285 here) accepts chunk 0; querying any other id throws with a message naming the missing `playgo-chunk.dat` and `playgo-scenario.json`, since the incomplete dump cannot establish whether that id exists; `APS5_PLAYGO_UNLISTED_CHUNKS=installed` answers every such id as installed (`LOCAL_FAST`, which PPSA15388 needs to load its saves) and `=absent` answers `SCE_PLAYGO_ERROR_BAD_CHUNK_ID` (which ends PPSA03001's id walk), as the user's choice for that dump. A `sce_sys` that holds other `playgo-*` files (`playgo-ficm.dat`, `playgo-hash-table.dat`) without `playgo-chunk.dat` or `playgo-scenario.json`, or a `playgo-chunk.dat` without a `plgx` header and a nonzero count, throws naming the file, because no such dump is known. These answers are unverified on a console, as is the console's answer for an id outside a package's chunk table. +- [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - the chunk set comes from `playgo-chunkdefs.xml` (every listed chunk plus chunks 0 through the default chunk), otherwise from `sce_sys/playgo-chunk.dat`, whose `plgx` header is read as holding the chunk count as a 16-bit value at offset 0x0A and ids 0 to count - 1 are taken as valid. The field was identified on four dumps, where exactly that many 0x20-byte entries follow from offset 0x100 (RIDE 4 PPSA01599: 35, Persona 5 Royal PPSA05110: 25, LEGO Horizon Adventures PPSA14632 and Kena PPSA01802: 1); the rest of the format is not decoded and ids are assumed to be contiguous. Every listed chunk is reported as installed (`LOCAL_FAST`). Without either file, a package with `sce_sys/playgo-scenario.json` (PPSA01748, whose dump lists no chunks) accepts ids 0 to 999 as installed, and `scePlayGoGetChunkId` lists the same ids. A title that builds its set of installed chunks from that list skips any chunk it does not see. The console's chunk limit is not known here, so an id from 1000 up is answered like an unlisted id of a package with no `playgo-*` file: it throws unless `APS5_PLAYGO_UNLISTED_CHUNKS` answers it (`=absent` ends PPSA02630's id walk, which otherwise asks for every id until one is rejected and wraps the 16-bit id). A package with no `playgo-*` file in `sce_sys` (PPSA03001, PPSA03490 and PPSA01285 here) accepts chunk 0; querying any other id throws with a message naming the missing `playgo-chunk.dat` and `playgo-scenario.json`, since the incomplete dump cannot establish whether that id exists; `APS5_PLAYGO_UNLISTED_CHUNKS=installed` answers every such id as installed (`LOCAL_FAST`, which PPSA15388 needs to load its saves) and `=absent` answers `SCE_PLAYGO_ERROR_BAD_CHUNK_ID` (which ends PPSA03001's id walk), as the user's choice for that dump. A `sce_sys` that holds other `playgo-*` files (`playgo-ficm.dat`, `playgo-hash-table.dat`) without `playgo-chunk.dat` or `playgo-scenario.json`, or a `playgo-chunk.dat` without a `plgx` header and a nonzero count, throws naming the file, because no such dump is known. These answers are unverified on a console, as is the console's answer for an id outside a package's chunk table. - [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - `scePlayGoGetSupportedOptionalChunk` and `scePlayGoGetOptionalChunk` report every bit set for `type` 0, 1 or 2, i.e. every optional chunk (language pack) supported and installed, like `scePlayGoGetLanguageMask`. Native requests confirm types 0 through 2 and an eight-byte result; their meanings and bit layout are still determined by the external service, not this module; PPSA01748 calls both with type 0 and hides every text language listed in `playgo-scenario.json` `chunkSupportedLanguages` when the bitmask is 0. The console's answer for a package that ships no optional chunks is unverified. From 780aa4472659e2ffaa28acefc1cce89da4cb6a79 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:38:09 -0700 Subject: [PATCH 048/428] fix(libkernel): block every word of a signal handler's mask while it runs main now keeps all four words of the guest signal mask. A handler's sa_mask above signal 32 is added too, and SIGKILL and SIGSTOP stay unblocked, as sigprocmask leaves them. --- .../libs/prx/libkernel/System/src/Signals.cpp | 17 +++++++++------ core/libs/tests/GuestSignalHandlerMask.cpp | 21 +++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 1fccd38522..c7b5993052 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -47,7 +47,7 @@ constexpr int SaNodefer = 0x10; constexpr int SaSiginfo = 0x40; constexpr int SaDefinedFlags = 0x7f; std::atomic handlers[MaxSignal + 1]{}; -std::atomic handlerMasks[MaxSignal + 1]{}; +std::atomic handlerMasks[MaxSignal + 1][4]{}; std::atomic handlerFlags[MaxSignal + 1]{}; std::atomic handlerReset[MaxSignal + 1]{}; static_assert(std::atomic::is_always_lock_free); @@ -163,12 +163,17 @@ void ResetToDefault(int guest) { } void RunHandler(int guest, GuestHandler handler) { const int flags = handlerFlags[guest].load(); - std::uint32_t mask = handlerMasks[guest].load(); - if ((flags & SaNodefer) == 0 && guest <= 32) mask |= SignalBit(guest); + std::uint32_t mask[4]; + for (int word = 0; word < 4; ++word) mask[word] = handlerMasks[guest][word].load(); + if ((flags & SaNodefer) == 0) mask[SignalWord(guest)] |= SignalBit(guest); + mask[0] &= ~UnblockableSignals; if ((flags & SaResethand) != 0) ResetToDefault(guest); - const std::uint32_t added = mask & ~blockedMask.fetch_or(mask); + std::uint32_t added[4]; + added[0] = mask[0] & ~blockedMask.fetch_or(mask[0]); + for (int word = 1; word < 4; ++word) added[word] = mask[word] & ~blockedMaskExtra[word - 1].fetch_or(mask[word]); handler(guest); - blockedMask.fetch_and(~added); + blockedMask.fetch_and(~added[0]); + for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_and(~added[word]); } void Dispatch(int native) { int guest = 0; @@ -191,7 +196,7 @@ bool Delivered(const GuestSigaction& action) { } bool Install(int guest, const GuestSigaction& action) { handlerReset[guest].store(false); - handlerMasks[guest].store(action.mask.bits[0]); + for (int word = 0; word < 4; ++word) handlerMasks[guest][word].store(action.mask.bits[word]); handlerFlags[guest].store(action.flags); const int native = NativeSignal(guest); if (!native) return true; diff --git a/core/libs/tests/GuestSignalHandlerMask.cpp b/core/libs/tests/GuestSignalHandlerMask.cpp index d118ab5fea..855372e0c2 100644 --- a/core/libs/tests/GuestSignalHandlerMask.cpp +++ b/core/libs/tests/GuestSignalHandlerMask.cpp @@ -21,6 +21,7 @@ struct GuestSigaction { GuestSignalSet mask; }; +static constexpr int SigKill = 9; static constexpr int SigTerm = 15; static constexpr int SigUsr1 = 30; static constexpr int SigUsr2 = 31; @@ -71,6 +72,13 @@ static void APS5_VABI BlockFromOtherThread(int) { }).join(); } +static GuestSignalSet setInHandler{}; + +static void APS5_VABI RecordMask(int) { + ++calls; + Require(sigprocmask_nid_postfix(SigBlock, nullptr, &setInHandler) == 0); +} + static void Install(int signal, void (APS5_VABI* handler)(int), int flags, std::uint32_t mask) { GuestSigaction action{reinterpret_cast(handler), flags, {{mask, 0, 0, 0}}}; Require(sigaction_nid_postfix(signal, &action, nullptr) == 0); @@ -119,6 +127,18 @@ static void CheckOtherThreadKept() { Require(CurrentMask() == 0); } +static void CheckUpperMaskWords() { + calls = 0; + const GuestSigaction action{reinterpret_cast(RecordMask), SaRestart, {{Bit(SigKill), 1u << 7, 0, 1u << 31}}}; + Require(sigaction_nid_postfix(SigUsr1, &action, nullptr) == 0); + Require(raise_nid_postfix(SigUsr1) == 0); + Require(calls == 1); + Require(setInHandler.bits[0] == Bit(SigUsr1) && setInHandler.bits[1] == 1u << 7 && setInHandler.bits[2] == 0 && setInHandler.bits[3] == 1u << 31); + GuestSignalSet after{}; + Require(sigprocmask_nid_postfix(SigBlock, nullptr, &after) == 0); + Require(after.bits[0] == 0 && after.bits[1] == 0 && after.bits[2] == 0 && after.bits[3] == 0); +} + static void CheckLeftWithLongjmp() { calls = 0; Install(SigUsr1, LeaveWithLongjmp, SaRestart, Bit(SigUsr2)); @@ -138,6 +158,7 @@ int main() { CheckNodefer(); CheckResethand(); CheckOtherThreadKept(); + CheckUpperMaskWords(); CheckLeftWithLongjmp(); return 0; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 834397222d..2d9867a247 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -212,7 +212,7 @@ - [sigtimedwait](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) waits on a process-wide pending set instead of throwing: a signal raised while blocked stays pending and is taken by `sigwait`, `sigwaitinfo` or `sigtimedwait`, delivered to the handler when `_sigprocmask` unblocks it, or delivered by `pause`. The set covers all 128 signals and the lowest number that is pending and in the set is selected (FreeBSD specifies the lowest number only among real-time signals). Selection, `EAGAIN` on timeout, `EINVAL` for a `tv_nsec` outside 0 to 1000000000 while nothing is pending and `NULL` timeout blocking indefinitely follow FreeBSD's [sigtimedwait(2)](https://man.freebsd.org/cgi/man.cgi?query=sigtimedwait). `sigwait` returns 0 with the number in its output, as POSIX specifies, and `sigwaitinfo` and `sigtimedwait` return the number, as FreeBSD does. The guest `siginfo_t` follows FreeBSD 11's `sys/sys/siginfo.h`: 80 bytes, `si_signo` at 0, `si_code` at 8, `si_pid` at 12, `si_uid` at 16, `si_addr` at 24, `si_value` at 32 and `si_band` at 40; only `si_signo`, `si_code` (`SI_USER`) and `si_pid` (the guest pid) are written, `si_uid` stays 0 because no uid is modelled, and the console's layout is not verified. PPSA85300 imports `sigtimedwait` and fails to load without it; its only call site is VLC's `vlc_writev` ([filesystem.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/posix/filesystem.c#L243-L278)), which polls with a zero timeout to drain a `SIGPIPE` only after `writev` fails with `EPIPE`. The waits are not cancellation points and an unblocked caught signal does not interrupt them with `EINTR`, where FreeBSD's `sigwaitinfo` and `sigtimedwait` may return it. `SIGPIPE` has no host equivalent and is never raised by a host write here (see `read, write and vector I/O`), so VLC's drain finds nothing - [pause, sigsuspend, sigwait, sigwaitinfo](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) take the pending set of `sigtimedwait` instead of throwing. `pause` waits for a pending signal that is not blocked and whose disposition can be delivered, runs its handler on the calling thread and returns -1 with `EINTR`; an ignored signal is discarded and the wait continues, and it never returns for a signal whose handler cannot run. A signal whose registered handler has `SA_SIGINFO` still throws, and one whose disposition is the default action and that has no host equivalent (every signal except 2, 4, 6, 8, 11 and 15) is neither delivered nor discarded: it stays pending until a wait takes it, where FreeBSD runs the default action, which terminates the process for most of them. A pending `SIGALRM` with the default disposition exits the process with status 142 once it is unblocked, as FreeBSD's default action does. PPSA68091, PPSA99169, PPSA99420 and PPSA97358 import them and fail to load without them. PPSA68091 calls `pause` at one site (0x8F7266), in a loop that waits while a global holds another thread's `errno` address. PPSA99169 only stores the address of `pause` in a table of symbol addresses (0x30DB1). PPSA99420 calls `pause` at 0x3BD80F6 and 0x3C9E220, `sigwait` at 0x3C9E4C7 and `sigwaitinfo` at 0x3C9E588. PPSA97358 calls `sigwait` from QEMU's `sigwait_compat` thread ([compatfd.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/compatfd.c#L28-L53)), which `qemu_signal_init` ([main-loop.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/main-loop.c#L110-L147)) starts for `SIGIO`, `SIGALRM` and `SIGBUS` when the main loop is created, so it reaches the wait at start-up, and from the vCPU thread of the dummy accelerator ([dummy-cpus.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/accel/dummy-cpus.c#L48-L56)). `sigsuspend` swaps the process-wide mask of `_sigprocmask` for its argument, waits as `pause` does through the same helper, then restores the mask through `_sigprocmask`, and returns -1 with `EINTR`; a null set throws, as the console faults in libthr instead of returning `EFAULT`. All four words of the argument replace the mask and are restored afterwards, as in `_sigprocmask`. Java 25 LAN / DNS (PPSA99869, #5501) imports it and fails to load without it, after `sysctlnametomib`, `fegetenv` and `mincore`. None of the five waits is a cancellation point - [getsid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns the calling process ID for pid 0 and the calling process, and `ESRCH` for other process IDs, assuming the single guest process is its own session leader. PPSA74126 imports it and fails to load without it -- [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do. While a handler runs, its own signal (unless `SA_NODEFER`) and the first 32 signals of its mask are added to the process-wide mask, and `SA_RESETHAND` resets the disposition to the default before it runs, as FreeBSD's `postsig_done` does. When the handler returns, only the signals it added are unblocked again, so a mask change another thread made meanwhile is kept, but so is one the handler made itself, which FreeBSD's `sigreturn` would undo. A handler left with `longjmp`, or with `siglongjmp` to a buffer that saved the mask, gets back the mask that buffer saved, as on FreeBSD; a signal raised meanwhile stays pending and is delivered when the handler returns to `raise`, but one left pending by a handler that a host signal ran asynchronously waits for the next `raise` or `_sigprocmask`. `SA_ONSTACK` is not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating +- [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do. While a handler runs, its own signal (unless `SA_NODEFER`) and the signals of its mask other than `SIGKILL` and `SIGSTOP` are added to the process-wide mask, and `SA_RESETHAND` resets the disposition to the default before it runs, as FreeBSD's `postsig_done` does. When the handler returns, only the signals it added are unblocked again, so a mask change another thread made meanwhile is kept, but so is one the handler made itself, which FreeBSD's `sigreturn` would undo. A handler left with `longjmp`, or with `siglongjmp` to a buffer that saved the mask, gets back the mask that buffer saved, as on FreeBSD; a signal raised meanwhile stays pending and is delivered when the handler returns to `raise`, but one left pending by a handler that a host signal ran asynchronously waits for the next `raise` or `_sigprocmask`. `SA_ONSTACK` is not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating - [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console - [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console From d3370bf57a1f8ebdf32afa074f46061e459f6c58 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:38:10 -0700 Subject: [PATCH 049/428] fix(libc): spell the jump buffer's mask calls for Mach-O hosts --- core/libs/prx/libc/src/JumpBuffer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/JumpBuffer.cpp b/core/libs/prx/libc/src/JumpBuffer.cpp index 551a11c089..17c34cf9d0 100644 --- a/core/libs/prx/libc/src/JumpBuffer.cpp +++ b/core/libs/prx/libc/src/JumpBuffer.cpp @@ -46,8 +46,8 @@ void APS5_VABI JumpBufferRestoreMask_nid_no_patch(const unsigned char* buffer) { // return address, rbx, rsp, rbp, r12-r15, MXCSR, the x87 control word and, as FreeBSD's do, the // signal mask at 72 with the flag sigsetjmp keeps at 88, once libkernel has registered sigprocmask. -#ifdef _WIN32 -#define APS5_JUMP_ASM_CALL(name) " call " name "\n" +#if defined(_WIN32) || defined(__APPLE__) +#define APS5_JUMP_ASM_CALL(name) " call " APS5_ASM_SYMBOL(name) "\n" #else #define APS5_JUMP_ASM_CALL(name) " call " name "@PLT\n" #endif From b39956b4e1d8f86180843c1da76879d39d321606 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:04:54 -0700 Subject: [PATCH 050/428] fix(libc): keep the signedness of narrow scanf conversions on Windows A %u, %o or %x scanned through the 64-bit temporary now writes an unsigned long long and is stored through an unsigned destination, as the rewritten %llu, %llo and %llx require. --- core/libs/prx/libc/include/WindowsScanning.hpp | 16 ++++++++++++---- core/libs/tests/Formatting.cpp | 9 +++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsScanning.hpp b/core/libs/prx/libc/include/WindowsScanning.hpp index aa71506809..420d8ba000 100644 --- a/core/libs/prx/libc/include/WindowsScanning.hpp +++ b/core/libs/prx/libc/include/WindowsScanning.hpp @@ -19,7 +19,9 @@ inline int ScanWindowsArguments_nid_no_patch(const char* format, const void* sou struct NarrowInteger { void* destination; size_t size; + bool isUnsigned; long long value; + unsigned long long unsignedValue; int end; }; std::vector narrowIntegers; @@ -58,10 +60,12 @@ inline int ScanWindowsArguments_nid_no_patch(const char* format, const void* sou translated += *format++; } if (narrow) { - narrowIntegers.push_back({args.Next(), length.empty() ? sizeof(int) : length == "h" ? sizeof(short) : sizeof(char), 0, -1}); + const bool isUnsigned = std::strchr("ouxX", conversion) != nullptr; + narrowIntegers.push_back({args.Next(), length.empty() ? sizeof(int) : length == "h" ? sizeof(short) : sizeof(char), isUnsigned, 0, 0, -1}); translated += "%n"; - pointers.push_back(&narrowIntegers.back().value); - pointers.push_back(&narrowIntegers.back().end); + auto& integer = narrowIntegers.back(); + pointers.push_back(isUnsigned ? static_cast(&integer.unsignedValue) : static_cast(&integer.value)); + pointers.push_back(&integer.end); } else if (!suppressed) pointers.push_back(args.Next()); else if (conversion != 'n') { translated += "%n"; @@ -71,7 +75,11 @@ inline int ScanWindowsArguments_nid_no_patch(const char* format, const void* sou const int result = scan(translated.c_str(), reinterpret_cast(pointers.data())); for (const auto& integer : narrowIntegers) { if (integer.end < 0) continue; - if (integer.size == sizeof(char)) *static_cast(integer.destination) = static_cast(integer.value); + if (integer.isUnsigned) { + if (integer.size == sizeof(char)) *static_cast(integer.destination) = static_cast(integer.unsignedValue); + else if (integer.size == sizeof(short)) *static_cast(integer.destination) = static_cast(integer.unsignedValue); + else *static_cast(integer.destination) = static_cast(integer.unsignedValue); + } else if (integer.size == sizeof(char)) *static_cast(integer.destination) = static_cast(integer.value); else if (integer.size == sizeof(short)) *static_cast(integer.destination) = static_cast(integer.value); else *static_cast(integer.destination) = static_cast(integer.value); } diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index e185038394..3b5f4e37c6 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -145,6 +145,15 @@ static void CheckIntegerScanWidths() { Require(sscanf_nid_postfix("5000000000", "%hd", &shortValue) == 1 && shortValue == -3584); signed char charValue = 7; Require(sscanf_nid_postfix("300", "%hhd", &charValue) == 1 && charValue == 44); + Require(sscanf_nid_postfix("4294967295", "%u", &unsignedValue) == 1 && unsignedValue == 4294967295u); + unsigned short unsignedShort = 7; + Require(sscanf_nid_postfix("40000", "%hu", &unsignedShort) == 1 && unsignedShort == 40000); + Require(sscanf_nid_postfix("70000", "%hu", &unsignedShort) == 1 && unsignedShort == 4464); + Require(sscanf_nid_postfix("ffff", "%hx", &unsignedShort) == 1 && unsignedShort == 0xffff); + unsigned char unsignedChar = 7; + Require(sscanf_nid_postfix("200", "%hhu", &unsignedChar) == 1 && unsignedChar == 200); + Require(sscanf_nid_postfix("300", "%hhu", &unsignedChar) == 1 && unsignedChar == 44); + Require(sscanf_nid_postfix("377", "%hho", &unsignedChar) == 1 && unsignedChar == 255); long long quad = 7; Require(sscanf_nid_postfix("3000000000", "%qd", &quad) == 1 && quad == 3000000000ll); unsigned long long unsignedQuad = 7; From 312fb247a9b8f63c3ce03e5f11d3b0b9bfcbf342 Mon Sep 17 00:00:00 2001 From: fablevoid <340827264+fable-void@users.noreply.github.com> Date: Mon, 19 Oct 2026 02:27:04 +0000 Subject: [PATCH 051/428] perf(libc): map a run of shared guest pages without per-page queries Mapping shared guest memory page by page queried the address space three times for every 16 KiB page: twice to find the placeholder the page splits from, and once more to see whether the section page still had to be committed, which it then committed on its own. For a range of fresh direct memory that is seven system calls a page, and with shared imports attached every direct mapping takes this path on the guest thread that maps it, under the mapping lock. A mapping that goes page by page now commits its section range once through one window, and splits each page off the placeholder the previous page left, whose extent it already knows; only a page after a resident run or a demotion looks the placeholder up again. A page now costs a placeholder split, the view and its protection. Tested: built; windows_write_watch gains a case that maps 256 shared pages and counts the address-space queries the mapping makes, then stores to and reads back every page. With the per-page lookups it fails (775 queries for 256 pages) and with this change it passes. agc_driver_committed_imports, agc_write_watch_coverage, agc_private_collect, agc_write_guards, agc_driver_dcc_keys_first_write, agc_driver_residency_dirty_mirrors, agc_driver_residency_driver, guest_memory and the host import, window import and alias store tests pass. --- .../libs/prx/libc/include/WindowsMappings.hpp | 43 ++++++++++++++++--- core/libs/tests/WindowsWriteWatch.cpp | 20 +++++++++ 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 624685e8a9..64e3b76ceb 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +29,10 @@ public: return *mappings; } + static std::uint64_t MemoryQueries() { + return queries.load(std::memory_order_relaxed); + } + void* Reserve(void* address, std::size_t bytes) { return allocate(GetCurrentProcess(), address, bytes, MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, nullptr, 0); } @@ -105,9 +110,14 @@ public: const auto shared = slot.page.lock(); return resident && !slot.stored && (!shared || shared->aliases.empty()); }; + const bool perPage = !resident; + if (perPage) commitSection(section, offset, bytes); + std::uintptr_t rest = 0; + std::uintptr_t restEnd = 0; for (std::size_t done = 0; done < bytes;) { const auto base = cursor + done; if (single(done)) { + rest = 0; auto runBytes = pageBytes; while (done + runBytes < bytes && (base + runBytes) % chunkBytes != 0 && single(done + runBytes)) runBytes += pageBytes; split(base, runBytes); @@ -138,11 +148,18 @@ public: const auto shared = pageOf(*slot); const auto aliases = shared->aliases; for (const auto alias : aliases) { - if (views.at(alias).resident) demote(alias); + if (!views.at(alias).resident) continue; + demote(alias); + rest = 0; } const auto viewBytes = std::min(pageBytes, sectionBytes - done); - split(base, pageBytes); - mapShared(base, section, offset + done, protection, viewProtection, viewBytes); + if (base == rest && base + pageBytes <= restEnd) { + if (base + pageBytes != restEnd && !VirtualFree(reinterpret_cast(base), pageBytes, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("split guest placeholder suffix"); + } else { + restEnd = split(base, pageBytes); + } + rest = base + pageBytes; + mapShared(base, section, offset + done, protection, viewProtection, viewBytes, perPage); slot->stored = true; shared->aliases.push_back(base); views.emplace(base, View{shared, protection, {}, 0, owned, offset + done, 0, 0, false, slot, hostPages(viewBytes)}); @@ -694,12 +711,23 @@ private: return (bytes + subpageBytes - 1) & ~(subpageBytes - 1); } - void mapShared(std::uintptr_t base, HANDLE section, std::uint64_t offset, DWORD protection, DWORD viewProtection = PAGE_EXECUTE_READWRITE, std::size_t sectionBytes = pageBytes) { + void commitSection(HANDLE section, std::uint64_t offset, std::size_t bytes) { + auto window = open(section, offset, bytes); + const auto memory = query(reinterpret_cast(window.bytes)); + if ((memory.State != MEM_COMMIT || memory.RegionSize < bytes) && !VirtualAlloc(window.bytes, bytes, MEM_COMMIT, PAGE_READWRITE)) { + const auto error = GetLastError(); + close(window); + throw std::system_error(static_cast(error), std::system_category(), "commit direct memory pages"); + } + close(window); + } + + void mapShared(std::uintptr_t base, HANDLE section, std::uint64_t offset, DWORD protection, DWORD viewProtection = PAGE_EXECUTE_READWRITE, std::size_t sectionBytes = pageBytes, bool committed = false) { void* page = reinterpret_cast(base); const auto viewBytes = hostPages(sectionBytes); if (viewBytes != pageBytes) split(base, viewBytes); if (!map(section, GetCurrentProcess(), page, offset, sectionBytes, MEM_REPLACE_PLACEHOLDER, viewProtection, nullptr, 0)) fail("map shared guest page"); - if (query(base).State == MEM_RESERVE && !VirtualAlloc(page, viewBytes, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) fail("commit shared guest page"); + if (!committed && query(base).State == MEM_RESERVE && !VirtualAlloc(page, viewBytes, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) fail("commit shared guest page"); DWORD previous; if (!VirtualProtect(page, viewBytes, protection, &previous)) fail("protect shared guest page"); if (viewBytes == pageBytes) return; @@ -890,11 +918,12 @@ private: static MEMORY_BASIC_INFORMATION query(std::uintptr_t address) { MEMORY_BASIC_INFORMATION memory{}; + queries.fetch_add(1, std::memory_order_relaxed); if (VirtualQuery(reinterpret_cast(address), &memory, sizeof(memory)) != sizeof(memory)) fail("query guest memory"); return memory; } - static void split(std::uintptr_t address, std::size_t bytes) { + static std::uintptr_t split(std::uintptr_t address, std::size_t bytes) { auto memory = query(address); memory = query(reinterpret_cast(memory.AllocationBase)); if (memory.State != MEM_RESERVE) throw std::runtime_error("guest mapping requires a placeholder"); @@ -905,6 +934,7 @@ private: } if (memory.RegionSize < bytes) throw std::runtime_error("guest placeholder is too small"); if (memory.RegionSize != bytes && !VirtualFree(reinterpret_cast(address), bytes, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("split guest placeholder suffix"); + return address + memory.RegionSize; } void reset(std::uintptr_t address, std::size_t bytes) { @@ -1023,6 +1053,7 @@ private: std::map splitAliases; std::size_t granularity = 0x10000; bool watchResident = false; + static inline std::atomic queries{0}; std::mutex mutex; AllocateFunction allocate = nullptr; MapFunction map = nullptr; diff --git a/core/libs/tests/WindowsWriteWatch.cpp b/core/libs/tests/WindowsWriteWatch.cpp index b44c48af61..8d95c9e30a 100644 --- a/core/libs/tests/WindowsWriteWatch.cpp +++ b/core/libs/tests/WindowsWriteWatch.cpp @@ -218,6 +218,25 @@ void CheckConcurrentWrites() { } } +namespace { +void CheckSharedMapQueriesPerMapping() { + constexpr std::size_t Pages = 256; + constexpr std::size_t Span = Pages * 16384; + HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Span, nullptr); + auto* memory = static_cast(Mappings::Get().Reserve(nullptr, Span)); + Require(section && memory, "cannot allocate shared test memory"); + const auto before = Mappings::MemoryQueries(); + Mappings::Get().Map(memory, Span, section, 0, PAGE_READWRITE); + const auto queries = Mappings::MemoryQueries() - before; + for (std::size_t page = 0; page < Pages; ++page) memory[page * 16384 + 8] = static_cast(page); + for (std::size_t page = 0; page < Pages; ++page) Require(memory[page * 16384 + 8] == static_cast(page), "a shared page lost its store"); + Mappings::Get().Reset(memory, Span); + VirtualFree(memory, 0, MEM_RELEASE); + CloseHandle(section); + Require(queries < Pages, ("a shared mapping queried the address space for every page: " + std::to_string(queries) + " queries for " + std::to_string(Pages) + " pages").c_str()); +} +} + int main() { auto* handler = AddVectoredExceptionHandler(1, HandleWrite); try { @@ -230,6 +249,7 @@ int main() { CheckPrivate(); CheckConcurrentWrites(); CheckForgottenSectionKeepsNoPages(); + CheckSharedMapQueriesPerMapping(); std::cout << "Windows deferred write-watch tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From a16262ba76fa99b9d20dc3afe53f698db7ae43b7 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:03:25 +0000 Subject: [PATCH 052/428] fix(libc): commit a shared mapping's section at once only for whole sections Committing the section range through one read-write window before the pages are mapped works for direct and shared memory, whose sections are committable and at least as long as the mapping. A file mapping passes the bytes its file still has: the window can run past the end of a short file, and a read-only file section refuses a read-write window, so every file mapping would fail. Only mappings that pass no section size commit the range up front; file mappings keep committing each view as before. guest_memory's regular file mapping case maps a read-only file and a file tail shorter than a page on Windows. Adapts fable-void's 027bff519 to main's file mappings. --- core/libs/prx/libc/include/WindowsMappings.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 64e3b76ceb..68e2ac324f 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -110,7 +110,7 @@ public: const auto shared = slot.page.lock(); return resident && !slot.stored && (!shared || shared->aliases.empty()); }; - const bool perPage = !resident; + const bool perPage = !resident && sectionBytes == SIZE_MAX; if (perPage) commitSection(section, offset, bytes); std::uintptr_t rest = 0; std::uintptr_t restEnd = 0; From bdf9bfe14a49d0a0bc13ef02db15226321df9264 Mon Sep 17 00:00:00 2001 From: fablevoid <340827264+fable-void@users.noreply.github.com> Date: Mon, 19 Oct 2026 06:21:22 +0000 Subject: [PATCH 053/428] perf(libc): map a run of shared guest pages as one view Mapping shared direct memory while shared maps are imported placed one view per 16 KiB page: a placeholder split, a map and a protect for every page. A 1 GiB map made about 200,000 system calls, held the mapping thread for about 0.4 s and took and dropped the mappings mutex on every page, so threads taking write faults queued behind it. Map now maps each run of shared pages that need a view as one view: one placeholder split, one map at read-write-execute (the cap later guest protections need) and one protect per run, and builds the per-page bookkeeping under the mutex while yielding to waiting faults. The views move the run's page references instead of copying them, so releasing the run does not hold the mutex. Protections and write tracking change subpages inside the view as before. A view is split only where a release or a remap cuts it: reset unmaps the view, maps the two pieces again over the same section offsets and restores every 4 KiB protection, so armed write tracking and guest protections survive the split. An alias of a page elsewhere and a per-page protection leave the view whole. Tested: windows_write_watch gains a case that maps 1 GiB of shared memory (65536 views before, one after), then protects one page, aliases one page, arms write tracking on one page, releases one page and remaps another: the run splits only at the released and remapped pages (2 and 4 views), every other page keeps its bytes and protection, and the armed page still faults and is collected. It fails before ("a shared gibibyte was mapped as 65536 views instead of one") and passes after. agc_driver_committed_imports: the 1 GiB shared map and import takes 190-196 ms on the mapping thread instead of 418-427 ms, and the longest write fault on another thread stays at 46-97 us. Built; the write watch, committed import, guest memory, write guard, host and window import, residency and mapping tests pass (58 tests). --- .../libs/prx/libc/include/WindowsMappings.hpp | 103 +++++++++++++----- core/libs/tests/WindowsWriteWatch.cpp | 79 ++++++++++++++ 2 files changed, 153 insertions(+), 29 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 68e2ac324f..e573f3154c 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -112,12 +112,9 @@ public: }; const bool perPage = !resident && sectionBytes == SIZE_MAX; if (perPage) commitSection(section, offset, bytes); - std::uintptr_t rest = 0; - std::uintptr_t restEnd = 0; for (std::size_t done = 0; done < bytes;) { const auto base = cursor + done; if (single(done)) { - rest = 0; auto runBytes = pageBytes; while (done + runBytes < bytes && (base + runBytes) % chunkBytes != 0 && single(done + runBytes)) runBytes += pageBytes; split(base, runBytes); @@ -144,28 +141,34 @@ public: done += runBytes; continue; } - const auto slot = slotOf(done); - const auto shared = pageOf(*slot); - const auto aliases = shared->aliases; - for (const auto alias : aliases) { - if (!views.at(alias).resident) continue; - demote(alias); - rest = 0; + std::vector, std::shared_ptr>> pages; + pages.reserve((bytes - done) / pageBytes); + auto runBytes = std::size_t{0}; + do { + const auto slot = slotOf(done + runBytes); + const auto shared = pageOf(*slot); + const auto aliases = shared->aliases; + for (const auto alias : aliases) { + if (views.at(alias).resident) demote(alias); + } + pages.emplace_back(slot, shared); + runBytes += pageBytes; + } while (done + runBytes < bytes && !single(done + runBytes)); + if (!perPage) commitSection(section, offset + done, runBytes); + split(base, runBytes); + mapRun(base, runBytes, section, offset + done); + DWORD previous; + if (!VirtualProtect(reinterpret_cast(base), runBytes, protection, &previous)) fail("protect shared guest pages"); + collectableRanges.clear(); + for (std::size_t at = 0; at < runBytes; at += pageBytes) { + auto& [slot, shared] = pages[at / pageBytes]; + slot->stored = true; + shared->aliases.push_back(base + at); + invalidate(*shared); + views.emplace(base + at, View{std::move(shared), protection, {}, 0, owned, offset + done + at, 0, 0, false, std::move(slot)}); } - const auto viewBytes = std::min(pageBytes, sectionBytes - done); - if (base == rest && base + pageBytes <= restEnd) { - if (base + pageBytes != restEnd && !VirtualFree(reinterpret_cast(base), pageBytes, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("split guest placeholder suffix"); - } else { - restEnd = split(base, pageBytes); - } - rest = base + pageBytes; - mapShared(base, section, offset + done, protection, viewProtection, viewBytes, perPage); - slot->stored = true; - shared->aliases.push_back(base); - views.emplace(base, View{shared, protection, {}, 0, owned, offset + done, 0, 0, false, slot, hostPages(viewBytes)}); - remember(freshRanges, base, base + pageBytes); - invalidate(*shared); - done += pageBytes; + remember(freshRanges, base, base + runBytes); + done += runBytes; } } @@ -736,6 +739,47 @@ private: if (!VirtualProtect(zeros, pageBytes - viewBytes, protection, &previous)) fail("protect guest pages past the end of a file"); } + void mapRun(std::uintptr_t base, std::size_t bytes, HANDLE section, std::uint64_t offset) { + if (!map(section, GetCurrentProcess(), reinterpret_cast(base), offset, bytes, MEM_REPLACE_PLACEHOLDER, PAGE_EXECUTE_READWRITE, nullptr, 0)) fail("map a run of shared guest pages"); + } + + void breakView(std::uintptr_t at) { + const auto memory = query(at); + if (memory.State == MEM_FREE || memory.Type != MEM_MAPPED) return; + const auto start = reinterpret_cast(memory.AllocationBase); + if (start == at) return; + const auto first = views.find(start); + if (first == views.end()) return; + struct Part { + std::uintptr_t base; + std::uintptr_t end; + DWORD protection; + bool committed; + }; + std::vector parts; + auto stop = start; + for (;;) { + const auto part = query(stop); + if (part.AllocationBase != memory.AllocationBase) break; + const auto partEnd = reinterpret_cast(part.BaseAddress) + part.RegionSize; + parts.push_back({stop, partEnd, part.Protect, part.State == MEM_COMMIT}); + stop = partEnd; + } + const auto section = first->second.section; + const auto offset = first->second.offset; + if (!unmap(GetCurrentProcess(), reinterpret_cast(start), MEM_PRESERVE_PLACEHOLDER)) fail("unmap a run of shared guest pages to split it"); + split(start, at - start); + mapRun(start, at - start, section->handle, offset); + mapRun(at, stop - at, section->handle, offset + (at - start)); + for (const auto& part : parts) { + if (!part.committed) continue; + for (const auto [from, to] : {std::pair{part.base, std::min(part.end, at)}, std::pair{std::max(part.base, at), part.end}}) { + DWORD previous; + if (from < to && !VirtualProtect(reinterpret_cast(from), to - from, part.protection, &previous)) fail("protect a split run of shared guest pages"); + } + } + } + static bool blank(std::uintptr_t page) { const auto* words = reinterpret_cast(page); for (std::size_t i = 0; i < pageBytes / sizeof(std::uint64_t); ++i) { @@ -943,6 +987,8 @@ private: forgetClean(address, end); breakRun(address); breakRun(end); + breakView(address); + breakView(end); if (const auto run = residentRuns.find(address); run != residentRuns.end() && run->second.end == end) { const auto memory = query(address); if (reinterpret_cast(memory.AllocationBase) != address) throw std::runtime_error("cannot release part of a host allocation"); @@ -974,12 +1020,11 @@ private: if (allocationEnd > end || query(allocationEnd).AllocationBase == memory.AllocationBase) throw std::runtime_error("guest release truncates a host allocation"); if (memory.Type == MEM_MAPPED) { if (!unmap(GetCurrentProcess(), reinterpret_cast(cursor), MEM_PRESERVE_PLACEHOLDER)) fail("unmap shared guest page"); - const auto found = views.find(cursor); - if (found != views.end()) { - std::erase(found->second.page->aliases, cursor); - views.erase(found); - forget(freshRanges, cursor, cursor + pageBytes); + for (auto found = views.lower_bound(cursor); found != views.end() && found->first < allocationEnd;) { + std::erase(found->second.page->aliases, found->first); + found = views.erase(found); } + forget(freshRanges, cursor, allocationEnd); } else if (memory.Type == MEM_PRIVATE) { if (const auto run = residentRuns.find(cursor); run != residentRuns.end()) evict(run); if (!VirtualFree(reinterpret_cast(cursor), allocationEnd - cursor, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("release private guest memory"); diff --git a/core/libs/tests/WindowsWriteWatch.cpp b/core/libs/tests/WindowsWriteWatch.cpp index 8d95c9e30a..c29b5c7bbe 100644 --- a/core/libs/tests/WindowsWriteWatch.cpp +++ b/core/libs/tests/WindowsWriteWatch.cpp @@ -235,6 +235,84 @@ void CheckSharedMapQueriesPerMapping() { CloseHandle(section); Require(queries < Pages, ("a shared mapping queried the address space for every page: " + std::to_string(queries) + " queries for " + std::to_string(Pages) + " pages").c_str()); } + +std::size_t MappedViews(const unsigned char* begin, std::size_t bytes) { + std::set bases; + for (auto cursor = begin; cursor < begin + bytes;) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(cursor, &info, sizeof(info)) == sizeof(info), "view query failed"); + if (info.Type == MEM_MAPPED) bases.insert(info.AllocationBase); + cursor = static_cast(info.BaseAddress) + info.RegionSize; + } + return bases.size(); +} + +DWORD ProtectionAt(const unsigned char* address) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(address, &info, sizeof(info)) == sizeof(info), "protection query failed"); + return info.State == MEM_COMMIT ? info.Protect : 0; +} + +void CheckSharedRunIsOneView() { + constexpr std::size_t Gib = std::size_t{1} << 30; + HANDLE large = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, static_cast(Gib), nullptr); + auto* whole = static_cast(Mappings::Get().Reserve(nullptr, Gib)); + Require(large && whole, "cannot allocate a shared gibibyte"); + Mappings::Get().Map(whole, Gib, large, 0, PAGE_READWRITE); + const auto wholeViews = MappedViews(whole, Gib); + whole[Gib - 1] = 1; + Mappings::Get().Reset(whole, Gib); + VirtualFree(whole, 0, MEM_RELEASE); + CloseHandle(large); + Require(wholeViews == 1, ("a shared gibibyte was mapped as " + std::to_string(wholeViews) + " views instead of one").c_str()); + + constexpr std::size_t Page = 16384; + constexpr std::size_t Pages = 64; + constexpr std::size_t Span = Pages * Page; + HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Span + Page, nullptr); + auto* memory = static_cast(Mappings::Get().Reserve(nullptr, Span)); + auto* other = static_cast(Mappings::Get().Reserve(nullptr, Page)); + Require(section && memory && other, "cannot allocate shared test memory"); + Mappings::Get().Map(memory, Span, section, 0, PAGE_READWRITE); + for (std::size_t page = 0; page < Pages; ++page) memory[page * Page + 8] = static_cast(page + 1); + Require(MappedViews(memory, Span) == 1, "a shared run was not mapped as one view"); + Mappings::Get().SetProtection(reinterpret_cast(memory + 5 * Page), Page, PAGE_READONLY); + DWORD previous; + Require(VirtualProtect(memory + 5 * Page, Page, PAGE_READONLY, &previous), "guest protection of one page failed"); + Require(MappedViews(memory, Span) == 1, "a per-page protection split the run"); + Mappings::Get().Map(other, Page, section, 30 * Page, PAGE_READWRITE); + Require(MappedViews(memory, Span) == 1, "an alias of one page split the run"); + other[16] = 0x5a; + Require(memory[30 * Page + 16] == 0x5a, "an alias store did not reach the run"); + std::array collected{}; + std::size_t available = collected.size(); + Require(Mappings::Get().Collect(reinterpret_cast(memory + 50 * Page), Page, collected.data(), &available, true, Collection::All), "run collection failed"); + Require(ProtectionAt(memory + 50 * Page) == PAGE_READONLY, "collection did not arm the run's page"); + Mappings::Get().Reset(memory + 10 * Page, Page); + Require(MappedViews(memory, Span) == 2, ("a partial unmap left " + std::to_string(MappedViews(memory, Span)) + " views instead of two").c_str()); + Require(ProtectionAt(memory + 10 * Page) == 0, "the unmapped page is still mapped"); + Mappings::Get().Map(memory + 20 * Page, Page, section, Span, PAGE_READWRITE); + Require(MappedViews(memory, Span) == 4, ("a remap of one page left " + std::to_string(MappedViews(memory, Span)) + " views instead of four").c_str()); + Require(memory[20 * Page + 8] == 0, "the remapped page shows the old page"); + for (std::size_t page = 0; page < Pages; ++page) { + if (page == 10 || page == 20) continue; + Require(memory[page * Page + 8] == static_cast(page + 1), ("a split lost page " + std::to_string(page)).c_str()); + } + Require(ProtectionAt(memory + 5 * Page) == PAGE_READONLY && ProtectionAt(memory + 6 * Page) == PAGE_READWRITE, "a split lost a page protection"); + Require(ProtectionAt(memory + 50 * Page) == PAGE_READONLY, "a split dropped a write-tracking protection"); + const auto before = faults.load(); + memory[50 * Page + 8] = 0x77; + Require(faults == before + 1, "a write to an armed page of a split run did not fault"); + memory[51 * Page + 8] = 0x78; + available = collected.size(); + Require(Mappings::Get().Collect(reinterpret_cast(memory + 50 * Page), 2 * Page, collected.data(), &available, true, Collection::Armed), "armed collection failed"); + Require(available == 1 && collected[0] == memory + 50 * Page, "the split run lost a tracked write"); + Mappings::Get().Reset(memory, Span); + Mappings::Get().Reset(other, Page); + VirtualFree(memory, 0, MEM_RELEASE); + VirtualFree(other, 0, MEM_RELEASE); + CloseHandle(section); +} } int main() { @@ -250,6 +328,7 @@ int main() { CheckConcurrentWrites(); CheckForgottenSectionKeepsNoPages(); CheckSharedMapQueriesPerMapping(); + CheckSharedRunIsOneView(); std::cout << "Windows deferred write-watch tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From 1f19ce0f2abf679063630761c87469ace6064159 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:05:03 +0000 Subject: [PATCH 054/428] fix(libc): keep file mappings on one view per page A run view maps the section at read-write-execute over the whole run and commits the run through a read-write window. A file mapping passes the protection its file allows and the bytes its file still has, so a read-only file or a file that ends inside the run cannot be mapped that way. File mappings now take the per-page path they took before the run views: one view per page at the file's view protection, with the last page cut at the end of the file. Mappings that pass no section size, direct and shared memory, still map each run as one view. guest_memory's regular file mapping case maps a read-only file and a file tail shorter than a page on Windows. Adapts fable-void's 3d695fbba to main's file mappings. --- core/libs/prx/libc/include/WindowsMappings.hpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index e573f3154c..f394e187d1 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -141,6 +141,24 @@ public: done += runBytes; continue; } + if (sectionBytes != SIZE_MAX) { + const auto slot = slotOf(done); + const auto shared = pageOf(*slot); + const auto aliases = shared->aliases; + for (const auto alias : aliases) { + if (views.at(alias).resident) demote(alias); + } + const auto viewBytes = std::min(pageBytes, sectionBytes - done); + split(base, pageBytes); + mapShared(base, section, offset + done, protection, viewProtection, viewBytes); + slot->stored = true; + shared->aliases.push_back(base); + views.emplace(base, View{shared, protection, {}, 0, owned, offset + done, 0, 0, false, slot, hostPages(viewBytes)}); + remember(freshRanges, base, base + pageBytes); + invalidate(*shared); + done += pageBytes; + continue; + } std::vector, std::shared_ptr>> pages; pages.reserve((bytes - done) / pageBytes); auto runBytes = std::size_t{0}; From 0e4fca088f333972856d5f8f06163974ab061e22 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Tue, 20 Oct 2026 17:33:54 +0200 Subject: [PATCH 055/428] fix(ssl): release connections when their context terminates --- core/libs/prx/libSceSsl/CMakeLists.txt | 9 +- core/libs/prx/libSceSsl/Export.cpp | 4 +- core/libs/tests/SslContextLifetime.cpp | 182 +++++++++++++++++++++++++ 3 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 core/libs/tests/SslContextLifetime.cpp diff --git a/core/libs/prx/libSceSsl/CMakeLists.txt b/core/libs/prx/libSceSsl/CMakeLists.txt index cb39ff0e4c..8296a60735 100644 --- a/core/libs/prx/libSceSsl/CMakeLists.txt +++ b/core/libs/prx/libSceSsl/CMakeLists.txt @@ -90,9 +90,16 @@ if(BUILD_TESTING) target_link_libraries(guest_ssl_ps5_readers_tests PRIVATE libSceSsl libc) configure_windows_unwind(guest_ssl_ps5_readers_tests) add_test(NAME guest_ssl_ps5_readers COMMAND guest_ssl_ps5_readers_tests) + add_test_executable(guest_ssl_context_lifetime_tests ${LIBS_INCLUDE_DIR}/tests/SslContextLifetime.cpp) + target_include_directories(guest_ssl_context_lifetime_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ssl_context_lifetime_tests PRIVATE libc mbedtls mbedx509 mbedcrypto) + configure_windows_unwind(guest_ssl_context_lifetime_tests) + add_test(NAME guest_ssl_context_lifetime COMMAND guest_ssl_context_lifetime_tests) + set_tests_properties(guest_ssl_context_lifetime PROPERTIES TIMEOUT 30) if(WIN32) target_link_libraries(guest_ssl_transport_tests PRIVATE bcrypt ws2_32) - set_tests_properties(guest_ssl guest_ssl_load guest_ssl_ps5_readers guest_ssl_transport_kernel guest_ssl_transport_net PROPERTIES ENVIRONMENT_MODIFICATION + target_link_libraries(guest_ssl_context_lifetime_tests PRIVATE bcrypt ws2_32) + set_tests_properties(guest_ssl guest_ssl_load guest_ssl_ps5_readers guest_ssl_context_lifetime guest_ssl_transport_kernel guest_ssl_transport_net PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$;PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index bfc982dcdc..7e9d9292bf 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -657,7 +657,9 @@ int APS5_VABI sceSslInit_nid_postfix(std::uint64_t poolSize) { int APS5_VABI sceSslTerm_nid_postfix(int sslCtxId) { auto& state = State(); std::lock_guard lock(state.mutex); - if (!state.contexts.erase(sslCtxId)) return ErrorInvalidId; + if (!state.contexts.contains(sslCtxId)) return ErrorInvalidId; + std::erase_if(state.connections, [sslCtxId](const auto& entry) { return entry.second.context == sslCtxId; }); + state.contexts.erase(sslCtxId); return 0; } diff --git a/core/libs/tests/SslContextLifetime.cpp b/core/libs/tests/SslContextLifetime.cpp new file mode 100644 index 0000000000..198e1c7cbe --- /dev/null +++ b/core/libs/tests/SslContextLifetime.cpp @@ -0,0 +1,182 @@ +#include "prx/libSceSsl/Export.cpp" + +#include +#include +#include +#include +#include +#include + +extern "C" bool KernelLookupSocketNative_nid_no_patch(int, std::shared_ptr*, std::uintptr_t*) { + std::fputs("unexpected native socket lookup in SSL lifetime test\n", stderr); + std::abort(); +} + +namespace { + +void Check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +int CreateContext() { + const int id = sceSslInit_nid_postfix(1024 * 1024); + Check(id > 0, "context creation failed"); + return id; +} + +int CreateConnection(int context) { + const int id = sceSslCreateConnection(context, -1, "localhost"); + Check(id > 0, "connection creation failed"); + return id; +} + +void DeleteBeforeTerm() { + for (int cycle = 0; cycle < 3; ++cycle) { + const int context = CreateContext(); + const int connection = CreateConnection(context); + Check(sceSslDeleteConnection(connection) == 0, "delete before term failed"); + Check(sceSslTerm_nid_postfix(context) == 0, "term after delete failed"); + Check(State().connections.empty() && State().contexts.empty(), "delete-first cycle retained records"); + } + std::puts("SSL delete-first control: 3 cycles, no retained records"); +} + +void TermBeforeDelete() { + bool clean = true; + for (int cycle = 0; cycle < 3; ++cycle) { + const auto before = State().connections.size(); + const int context = CreateContext(); + const int connection = CreateConnection(context); + const auto created = State().connections.size(); + Check(sceSslTerm_nid_postfix(context) == 0, "term before delete failed"); + const auto terminated = State().connections.size(); + Check(sceSslDeleteConnection(connection) == ErrorInvalidId, "post-term delete result changed"); + const auto deleted = State().connections.size(); + Check(State().contexts.empty(), "context survived termination"); + clean = clean && before == 0 && created == 1 && terminated == 0 && deleted == 0; + std::printf("SSL term-first cycle %d: connections %zu -> %zu -> %zu -> %zu\n", cycle + 1, before, created, terminated, deleted); + } + Check(clean, "termination retained orphan connection records"); +} + +void SeparateContexts() { + const int first = CreateContext(); + const int second = CreateContext(); + const int firstConnection = CreateConnection(first); + const int secondConnection = CreateConnection(second); + const int extraConnection = CreateConnection(first); + std::weak_ptr lifetime = State().contexts.at(first); + Check(sceSslTerm_nid_postfix(first) == 0, "selective term failed"); + Check(lifetime.expired(), "idle terminated context survived"); + Check(State().connections.size() == 1 && State().connections.contains(secondConnection), "term did not isolate connection ownership"); + Check(!State().connections.contains(firstConnection) && !State().connections.contains(extraConnection), "term retained a child connection"); + Check(sceSslTerm_nid_postfix(first) == ErrorInvalidId, "repeated term result changed"); + Check(sceSslCreateConnection(first, -1, "localhost") == ErrorInvalidId, "create accepted a terminated context"); + Check(sceSslDeleteConnection(firstConnection) == ErrorInvalidId, "delete accepted a terminated connection"); + Check(sceSslSetVerifyCallback(firstConnection, nullptr, nullptr) == ErrorInvalidId, "callback accepted a terminated connection"); + Check(sceSslConnect(firstConnection) == ErrorInvalidId && sceSslClose(firstConnection) == ErrorInvalidId, "I/O accepted a terminated connection"); + Check(sceSslSetVerifyCallback(secondConnection, nullptr, nullptr) == 0, "term invalidated another context's connection"); + Check(sceSslDeleteConnection(secondConnection) == 0 && sceSslTerm_nid_postfix(second) == 0, "unrelated context cleanup failed"); + Check(State().connections.empty() && State().contexts.empty(), "selective term retained records"); +} + +void ConcurrentLifecycle() { + for (int cycle = 0; cycle < 32; ++cycle) { + const int context = CreateContext(); + std::array connections{}; + for (auto& id : connections) id = CreateConnection(context); + std::barrier start(3); + std::atomic valid{true}; + std::thread creator([&] { + start.arrive_and_wait(); + for (int attempt = 0; attempt < 16; ++attempt) { + const int id = sceSslCreateConnection(context, -1, "localhost"); + if (id == ErrorInvalidId) continue; + if (id <= 0) { valid = false; continue; } + const int result = sceSslDeleteConnection(id); + if (result != 0 && result != ErrorInvalidId) valid = false; + } + }); + std::thread deleter([&] { + start.arrive_and_wait(); + for (const auto id : connections) { + const int result = sceSslDeleteConnection(id); + if (result != 0 && result != ErrorInvalidId) valid = false; + } + }); + start.arrive_and_wait(); + const int result = sceSslTerm_nid_postfix(context); + creator.join(); + deleter.join(); + Check(result == 0 && valid, "concurrent lifecycle returned an unexpected error"); + Check(State().connections.empty() && State().contexts.empty(), "concurrent termination retained records"); + } + std::puts("SSL concurrent create/delete/term: 32 cycles, no retained records"); +} + +struct CallbackState { + std::latch entered{1}; + std::latch resume{1}; + int context = 0; + bool valid = false; +}; + +int APS5_VABI PendingCallback(int context, unsigned int, void**, int count, void* argument) { + auto& state = *static_cast(argument); + state.valid = context == state.context && count == 0; + state.entered.count_down(); + state.resume.wait(); + return 0; +} + +void InFlightSession() { + const int context = CreateContext(); + const int connection = CreateConnection(context); + auto owner = State().contexts.at(context); + std::weak_ptr contextLifetime = owner; + auto session = std::make_shared(context, owner, std::shared_ptr{}, 0); + { + PoolScope scope(owner->pool); + session->Configure("localhost", "", InitialVerifyOptions); + } + Check(owner->pool.current > 0, "session did not allocate backend state"); + session->verified = true; + session->verifyResult = ErrorCallbackCheck; + State().connections.at(connection).session = session; + std::weak_ptr sessionLifetime = session; + CallbackState callback; + callback.context = context; + Check(sceSslSetVerifyCallback(connection, PendingCallback, &callback) == 0, "callback setup failed"); + owner.reset(); + session.reset(); + int operationResult = ErrorInvalidId; + std::thread operation([&] { operationResult = sceSslConnect(connection); }); + callback.entered.wait(); + const int result = sceSslTerm_nid_postfix(context); + const bool removed = State().connections.empty() && State().contexts.empty(); + const bool retained = !sessionLifetime.expired() && !contextLifetime.expired(); + callback.resume.count_down(); + operation.join(); + Check(result == 0 && removed, "term did not remove the in-flight connection"); + Check(retained, "term destroyed an in-flight session or its pool"); + Check(callback.valid && operationResult == 0, "in-flight verification callback did not finish"); + Check(sessionLifetime.expired() && contextLifetime.expired(), "finished operation retained its session or pool"); + Check(sceSslConnect(connection) == ErrorInvalidId && sceSslDeleteConnection(connection) == ErrorInvalidId, "post-term connection became usable"); + std::puts("SSL in-flight callback completed; session and pool released after completion"); +} + +} + +int main() { + try { + DeleteBeforeTerm(); + TermBeforeDelete(); + SeparateContexts(); + ConcurrentLifecycle(); + InFlightSession(); + std::puts("SSL context lifetime passed"); + } catch (const std::exception& error) { + std::fprintf(stderr, "SSL context lifetime failed: %s\n", error.what()); + return 1; + } +} From f2173bba34562299f9d90583120ade2205ae12ef Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:23:30 +0200 Subject: [PATCH 056/428] docs(ssl): remove obsolete terminated connection ownership claims --- docs/dev/debt/libSceSsl.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index 23190fe3ef..0b18637a62 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -6,7 +6,7 @@ - [sceSslDisableOption, sceSslDisableOptionInternal, sceSslDisableOptionInternalInsecure, sceSslEnableOption, sceSslEnableOptionInternal, sceSslGetNanoSSLModuleId, sceSslGetPeerCert, sceSslGetSslError, sceSslLoadRootCACert and sceSslSetSslVersion](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - the list is the intersection of the export table of the console's libSceSsl (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo. Names and NIDs appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSsl.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); no credible prototypes were found. They are declared without parameters solely to resolve known imports, not as an ABI claim; callers must not call them until signatures are verified. - [sceSslRecv, sceSslReuseConnection and sceSslSend](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - the list is the intersection of the export table of the console's libSceSsl (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo. Names and NIDs appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSsl.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv). Their parameter shapes follow [SharpProspero's pinned bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs#L222-L262), and are not measured on a PS5. -- [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. The behaviour below was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title); what differs for a title process is unknown. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006) from `sceSslCreateConnection`, and every connection call on an unknown or deleted connection id or on a connection whose context was terminated returns `SSL_ERROR_INVALID_ID`. `sceSslCreateConnection` returns `SSL_ERROR_INVALID_VALUE` (0x8095F007) for a null host and does not check the socket: `sceSslConnect` on a descriptor that is not a socket returns `SCE_NET_ERROR_EBADF` (0x80410109). `sceSslConnect` runs a TLS 1.2 client handshake with Mbed TLS 3.6.7 over the host socket that the guest number maps to in the libkernel or libSceNet socket table, and `sceSslRead` and `sceSslWrite` carry application data over it. TLS 1.3 is compiled out: the console offers TLS 1.2 only (its ClientHello carries 12 suites and no supported_versions extension) and a TLS 1.3-only server gets `SSL_ERROR_FATAL_ALERT` (0x8095F010) from it, as from this build. Connection state is allocated from the context's pool and returned to it by `sceSslDeleteConnection`; a connection keeps the pool alive after `sceSslTerm`, whose connections return `SSL_ERROR_INVALID_ID` from then on. The console's pool use for one connection with a two-certificate chain peaked at 80,544 bytes in a 256 KiB pool (5,568 after `sceSslInit`, 12,000 after loading one CA); a 16 KiB pool fails the connect with `SSL_ERROR_OUT_OF_MEMORY` (0x809517D5) there and here. The server's chain is checked against the CAs loaded with `sceSslLoadCert`, the host name the connection was created with (case-insensitively; an IP literal sends no SNI and matches an IP subject alternative name; an empty host sends an empty SNI and fails `CN_CHECK`) and, when enabled, its validity dates, and `sceSslConnect` returns the first failure the connection's verify options select (the verify option entry below lists the bits, codes and initial options); no root bundle is shipped and the console's built-in roots are unknown, so with no CA loaded a connection with the initial options returns `SSL_ERROR_UNKNOWN_CA` (0x8095F00C), as on the console. A client certificate loaded with `sceSslLoadCert` is sent when the server asks for one. A failed verification is final: close_notify is sent, later `sceSslConnect` calls return the same code (also after the `WANT_POLLIN` rounds of a non-blocking socket), and `sceSslRead` and `sceSslWrite` throw, as the console's codes for them were not measured. With a verify callback set it is called once after the handshake, outside the connection's lock, with the context id, the bits of the enabled checks that failed (`KNOWN_CA_CHECK` 0x20, `CN_CHECK` 0x4, the date checks when enabled, 0 when the chain passed), handles to a copy of the peer chain as received (the leaf first, the root last when the server sent it) that the certificate readers of the connection's context accept until the connection is deleted (the handles stay registered in the context until it is terminated; the console frees them at the delete), the chain length and the user argument. A chain fault that no option names (`SSL_ERROR_INVALID_CERT` without a callback) throws when a callback is set, as the bits the console passes for it were not measured. A non-negative return accepts the connection whatever the bits were; a negative return fails the connect with `SSL_ERROR_APPLICATION_VERIFICATION_CALLBACK_CHECK` (0x8095F0FD) and the callback runs again, with the same chain, from every later `sceSslConnect`, `sceSslRead` and `sceSslWrite` of the connection, which return 0x8095F0FD while it keeps refusing and proceed once it accepts. A `sceSslConnect` from inside the callback, or from another thread while it runs, throws. `sceSslRead` and `sceSslWrite` before `sceSslConnect` run the handshake and verification themselves and return its code when it fails. A non-blocking socket that would block returns `SSL_ERROR_WANT_POLLIN` (0x8095F013) or `SSL_ERROR_WANT_POLLOUT` (0x8095F014) from `sceSslConnect`, `sceSslRead` and `sceSslWrite`; the next `sceSslConnect` resumes the handshake, and `sceSslWrite` returns the part it sent before blocking (the console returned 32 KiB of a 64 KiB write). A blocking libSceNet socket whose `SO_RCVTIMEO` expires returns `SSL_ERROR_WANT_POLLIN` as well, and the call can be repeated; `SSL_ERROR_READ_TIMEOUT` (0x80951710) is never returned. libkernel keeps its timeouts above the host socket, so they do not bound SSL reads and writes on a libkernel socket. `sceSslRead` with flags 0 reads until `len` bytes arrived or the stream ended and with flags 1 or 2 returns what the first record delivers. Native flags 3 peek without consuming data; values outside 0 through 3 return `SSL_ERROR_INVALID_VALUE` before connection lookup. A non-blocking read that got some bytes returns them. It returns 0 after the server's close_notify and after a transport that ends without one, every later read 0 again, and `SSL_ERROR_EOF` (0x8095177C) and `SSL_ERROR_SOCKET_CLOSED` (0x8095170D) never from a read. A reset (`ECONNRESET`, `EPIPE`, `WSAECONNRESET`, `WSAECONNABORTED`, `WSAENETRESET`) returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) from `sceSslRead` and `SCE_NET_ERROR_EPIPE` (0x80410120) from `sceSslWrite`, on every later call too; a record that fails its MAC returns 0x80951D55 from `sceSslRead` after the fatal alert goes out. `sceSslWrite` accepts flags 0 only, `SSL_ERROR_INVALID_VALUE` otherwise, sends all of `len`, capped at `INT_MAX`, and returns the count; it keeps writing after the peer's close_notify or a stream that ended without one, as the console does. A fatal alert during the handshake returns `SSL_ERROR_FATAL_ALERT` and ends the transport: later `sceSslConnect` calls return 0, `sceSslRead` 0 and `sceSslWrite` `SSL_ERROR_SOCKET_CLOSED`, as measured. A transport that ends without close_notify or a reset during the handshake returns `SSL_ERROR_SOCKET_CLOSED`, the session is dropped and the next `sceSslConnect` starts over; those two handshake cases and `SSL_ERROR_INVALID_CERT` (0x8095F00A) for a verification failure no option names were not measured. Other handshake and socket failures, a read or write after a failed verification, a write after `sceSslClose`, a null buffer and a read or write from inside the callback throw, as their console codes are unknown. A connection serializes its reads and writes, so a read blocked in one thread holds back a write on the same connection from another. `sceSslDeleteConnection` sends no close_notify and returns `SSL_ERROR_INVALID_ID` when repeated. `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, which then returns `SSL_ERROR_INVALID_ID`; a second `sceSslTerm` returns `SSL_ERROR_INVALID_ID` and `sceSslInit` with a pool size of 0 returns `SCE_KERNEL_ERROR_EINVAL` (0x80020016). `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0. The console's first `sceSslConnect` in a process took 23.8 s before its ClientHello left, a one-time initialisation not emulated. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts +- [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. The behaviour below was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title); what differs for a title process is unknown. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006) from `sceSslCreateConnection`, and every connection call on an unknown or deleted connection id or on a connection whose context was terminated returns `SSL_ERROR_INVALID_ID`. `sceSslCreateConnection` returns `SSL_ERROR_INVALID_VALUE` (0x8095F007) for a null host and does not check the socket: `sceSslConnect` on a descriptor that is not a socket returns `SCE_NET_ERROR_EBADF` (0x80410109). `sceSslConnect` runs a TLS 1.2 client handshake with Mbed TLS 3.6.7 over the host socket that the guest number maps to in the libkernel or libSceNet socket table, and `sceSslRead` and `sceSslWrite` carry application data over it. TLS 1.3 is compiled out: the console offers TLS 1.2 only (its ClientHello carries 12 suites and no supported_versions extension) and a TLS 1.3-only server gets `SSL_ERROR_FATAL_ALERT` (0x8095F010) from it, as from this build. Connection state is allocated from the context's pool and returned to it by `sceSslDeleteConnection`. The console's pool use for one connection with a two-certificate chain peaked at 80,544 bytes in a 256 KiB pool (5,568 after `sceSslInit`, 12,000 after loading one CA); a 16 KiB pool fails the connect with `SSL_ERROR_OUT_OF_MEMORY` (0x809517D5) there and here. The server's chain is checked against the CAs loaded with `sceSslLoadCert`, the host name the connection was created with (case-insensitively; an IP literal sends no SNI and matches an IP subject alternative name; an empty host sends an empty SNI and fails `CN_CHECK`) and, when enabled, its validity dates, and `sceSslConnect` returns the first failure the connection's verify options select (the verify option entry below lists the bits, codes and initial options); no root bundle is shipped and the console's built-in roots are unknown, so with no CA loaded a connection with the initial options returns `SSL_ERROR_UNKNOWN_CA` (0x8095F00C), as on the console. A client certificate loaded with `sceSslLoadCert` is sent when the server asks for one. A failed verification is final: close_notify is sent, later `sceSslConnect` calls return the same code (also after the `WANT_POLLIN` rounds of a non-blocking socket), and `sceSslRead` and `sceSslWrite` throw, as the console's codes for them were not measured. With a verify callback set it is called once after the handshake, outside the connection's lock, with the context id, the bits of the enabled checks that failed (`KNOWN_CA_CHECK` 0x20, `CN_CHECK` 0x4, the date checks when enabled, 0 when the chain passed), handles to a copy of the peer chain as received (the leaf first, the root last when the server sent it) that the certificate readers of the connection's context accept until the connection is deleted (the handles stay registered in the context until it is terminated; the console frees them at the delete), the chain length and the user argument. A chain fault that no option names (`SSL_ERROR_INVALID_CERT` without a callback) throws when a callback is set, as the bits the console passes for it were not measured. A non-negative return accepts the connection whatever the bits were; a negative return fails the connect with `SSL_ERROR_APPLICATION_VERIFICATION_CALLBACK_CHECK` (0x8095F0FD) and the callback runs again, with the same chain, from every later `sceSslConnect`, `sceSslRead` and `sceSslWrite` of the connection, which return 0x8095F0FD while it keeps refusing and proceed once it accepts. A `sceSslConnect` from inside the callback, or from another thread while it runs, throws. `sceSslRead` and `sceSslWrite` before `sceSslConnect` run the handshake and verification themselves and return its code when it fails. A non-blocking socket that would block returns `SSL_ERROR_WANT_POLLIN` (0x8095F013) or `SSL_ERROR_WANT_POLLOUT` (0x8095F014) from `sceSslConnect`, `sceSslRead` and `sceSslWrite`; the next `sceSslConnect` resumes the handshake, and `sceSslWrite` returns the part it sent before blocking (the console returned 32 KiB of a 64 KiB write). A blocking libSceNet socket whose `SO_RCVTIMEO` expires returns `SSL_ERROR_WANT_POLLIN` as well, and the call can be repeated; `SSL_ERROR_READ_TIMEOUT` (0x80951710) is never returned. libkernel keeps its timeouts above the host socket, so they do not bound SSL reads and writes on a libkernel socket. `sceSslRead` with flags 0 reads until `len` bytes arrived or the stream ended and with flags 1 or 2 returns what the first record delivers. Native flags 3 peek without consuming data; values outside 0 through 3 return `SSL_ERROR_INVALID_VALUE` before connection lookup. A non-blocking read that got some bytes returns them. It returns 0 after the server's close_notify and after a transport that ends without one, every later read 0 again, and `SSL_ERROR_EOF` (0x8095177C) and `SSL_ERROR_SOCKET_CLOSED` (0x8095170D) never from a read. A reset (`ECONNRESET`, `EPIPE`, `WSAECONNRESET`, `WSAECONNABORTED`, `WSAENETRESET`) returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) from `sceSslRead` and `SCE_NET_ERROR_EPIPE` (0x80410120) from `sceSslWrite`, on every later call too; a record that fails its MAC returns 0x80951D55 from `sceSslRead` after the fatal alert goes out. `sceSslWrite` accepts flags 0 only, `SSL_ERROR_INVALID_VALUE` otherwise, sends all of `len`, capped at `INT_MAX`, and returns the count; it keeps writing after the peer's close_notify or a stream that ended without one, as the console does. A fatal alert during the handshake returns `SSL_ERROR_FATAL_ALERT` and ends the transport: later `sceSslConnect` calls return 0, `sceSslRead` 0 and `sceSslWrite` `SSL_ERROR_SOCKET_CLOSED`, as measured. A transport that ends without close_notify or a reset during the handshake returns `SSL_ERROR_SOCKET_CLOSED`, the session is dropped and the next `sceSslConnect` starts over; those two handshake cases and `SSL_ERROR_INVALID_CERT` (0x8095F00A) for a verification failure no option names were not measured. Other handshake and socket failures, a read or write after a failed verification, a write after `sceSslClose`, a null buffer and a read or write from inside the callback throw, as their console codes are unknown. A connection serializes its reads and writes, so a read blocked in one thread holds back a write on the same connection from another. `sceSslDeleteConnection` sends no close_notify and returns `SSL_ERROR_INVALID_ID` when repeated. A second `sceSslTerm` returns `SSL_ERROR_INVALID_ID` and `sceSslInit` with a pool size of 0 returns `SCE_KERNEL_ERROR_EINVAL` (0x80020016). `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0. The console's first `sceSslConnect` in a process took 23.8 s before its ClientHello left, a one-time initialisation not emulated. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts - [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - takes the connection id, as in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252). Measured on 13.40 as the connection entry describes: it sends close_notify once if the handshake completed and returns 0, 0 again when repeated and 0 on a connection that never connected, already failed verification or lost its transport to a fatal alert; a close before the handshake changes nothing, so the connection can still connect. After the close `sceSslConnect` returns 0 and `sceSslRead` 0, as on the console; `sceSslWrite` after it throws, as the console's code was not measured. A close_notify that a non-blocking socket cannot take at once returns `SSL_ERROR_WANT_POLLOUT` (0x8095F014) and the close can be repeated; after a reset it returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) every time. The socket is left to the title; an unknown connection returns `SSL_ERROR_INVALID_ID` (0x8095F006). Whether the console closes the socket is unknown - [Certificate readers](../../../core/libs/prx/libSceSsl/Export.cpp): foreign and expired certificate/name handles throw instead of following native unchecked pointers. Name views track certificate lifetime and preserve identity; the native no-op name release is implemented. View storage remains allocated until context destruction. Numeric names for attributes Mbed TLS cannot identify may differ from native names. Fingerprint stack-space validation and backend digest failures are not reproduced. Signatures, valid-context/null-argument precedence, serial truncation, name copy bounds, validity timestamps and supported hash selectors are confirmed by the supplied module. - [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS parses DER/PEM chains. CA imports still commit together instead of preserving successful entries before a later failure. Client certificate replacement clears the previous identity; raw private keys load independently and are parsed when configuring TLS. Partial client chains on decode failure, native key parse errors and incomplete identities during TLS setup remain unsupported. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. Native CA queries include built-in roots; the host still queries loaded authorities and returns `NOT_FOUND` for an empty context. No native root bundle is copied. Null-output and invalid-context precedence, output preservation on invalid contexts and the SDK 7.50 unload error boundary are recovered. Legacy clearing of foreign raw CA lists remains unchanged. Certificate reader queries reflect active certificates or outstanding snapshots. Loading credentials does not start a TLS connection; `sceSslConnect` verifies the server against the loaded CAs and offers the loaded client certificate From 56b2a6393f9cc32706fd98f2c841f5aa81165c95 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:39:02 -0700 Subject: [PATCH 057/428] fix(libkernel): keep another thread's block of a signal a running handler also blocks A handler's blocks are now holds kept apart from the mask sigprocmask sets, and released when the handler returns, so a sigprocmask(SIG_BLOCK) from another thread meanwhile survives even for a signal in the handler's sa_mask. sigprocmask reports the holds; SIG_UNBLOCK of a held signal releases its hold and SIG_SETMASK, which longjmp uses, releases them all, while sigsuspend's own restore keeps them. --- .../libs/prx/libkernel/System/src/Signals.cpp | 104 +++++++++++------- core/libs/tests/GuestSignalHandlerMask.cpp | 31 ++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 99 insertions(+), 38 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index c7b5993052..31f01e4ae3 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -53,6 +53,8 @@ std::atomic handlerReset[MaxSignal + 1]{}; static_assert(std::atomic::is_always_lock_free); std::atomic blockedMask{0}; std::atomic blockedMaskExtra[3]{}; +std::atomic handlerHolds[MaxSignal + 1]{}; +constexpr std::uint64_t HoldCount = 0xffffffffu; static_assert(std::atomic::is_always_lock_free); std::mutex registration; GuestSigaction dispositions[MaxSignal + 1]{}; @@ -91,11 +93,29 @@ std::uint32_t SignalBit(int guest) { return 1u << ((guest - 1) % 32); } std::uint32_t SignalWord(int guest) { return static_cast((guest - 1) / 32); } bool SignalInSet(const GuestSignalSet& set, int guest) { return (set.bits[SignalWord(guest)] & SignalBit(guest)) != 0; } bool SetHasSignal(const GuestSignalSet& set) { return (set.bits[0] | set.bits[1] | set.bits[2] | set.bits[3]) != 0; } +bool HeldByHandler(int guest) { return (handlerHolds[guest].load() & HoldCount) != 0; } +void ReleaseHold(int guest, std::uint32_t epoch) { + auto value = handlerHolds[guest].load(); + while ((value >> 32) == epoch && (value & HoldCount) != 0 && !handlerHolds[guest].compare_exchange_weak(value, value - 1)) {} +} +void ClearHold(int guest) { + auto value = handlerHolds[guest].load(); + while ((value & HoldCount) != 0 && !handlerHolds[guest].compare_exchange_weak(value, ((value >> 32) + 1) << 32)) {} +} +std::uint32_t HeldWord(int word) { + std::uint32_t held = 0; + for (int bit = 0; bit < 32; ++bit) + if (HeldByHandler(word * 32 + bit + 1)) held |= 1u << bit; + return held; +} +std::uint32_t BlockedWord(int word) { + return (word == 0 ? blockedMask.load() : blockedMaskExtra[word - 1].load()) | HeldWord(word); +} bool SignalBlocked(int guest) { if (guest < 1 || guest > MaxSignal) return false; const auto word = SignalWord(guest); const auto mask = word == 0 ? blockedMask.load() : blockedMaskExtra[word - 1].load(); - return (mask & SignalBit(guest)) != 0; + return (mask & SignalBit(guest)) != 0 || HeldByHandler(guest); } struct PendingSignals { @@ -168,12 +188,12 @@ void RunHandler(int guest, GuestHandler handler) { if ((flags & SaNodefer) == 0) mask[SignalWord(guest)] |= SignalBit(guest); mask[0] &= ~UnblockableSignals; if ((flags & SaResethand) != 0) ResetToDefault(guest); - std::uint32_t added[4]; - added[0] = mask[0] & ~blockedMask.fetch_or(mask[0]); - for (int word = 1; word < 4; ++word) added[word] = mask[word] & ~blockedMaskExtra[word - 1].fetch_or(mask[word]); + std::uint32_t epochs[MaxSignal + 1]{}; + for (int held = 1; held <= MaxSignal; ++held) + if (mask[SignalWord(held)] & SignalBit(held)) epochs[held] = static_cast(handlerHolds[held].fetch_add(1) >> 32); handler(guest); - blockedMask.fetch_and(~added[0]); - for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_and(~added[word]); + for (int held = 1; held <= MaxSignal; ++held) + if (mask[SignalWord(held)] & SignalBit(held)) ReleaseHold(held, epochs[held]); } void Dispatch(int native) { int guest = 0; @@ -520,6 +540,43 @@ constexpr std::size_t MinSignalStackSize = 2048; thread_local GuestStack alternateStack{nullptr, 0, SsDisable}; } +namespace { + +int ChangeBlockedMask(int how, const GuestSignalSet* set, GuestSignalSet* previousSet, bool releaseHolds) { + if (set != nullptr && (how < 1 || how > 3)) { *__error_nid_postfix() = 22; return -1; } + { + std::lock_guard lock(registration); + if (previousSet != nullptr) + for (int word = 0; word < 4; ++word) previousSet->bits[word] = BlockedWord(word); + if (set != nullptr) { + switch (how) { + case 1: + blockedMask.fetch_or(set->bits[0] & ~UnblockableSignals); + for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_or(set->bits[word]); + break; + case 2: + blockedMask.fetch_and(~set->bits[0]); + for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_and(~set->bits[word]); + if (releaseHolds) + for (int guest = 1; guest <= MaxSignal; ++guest) + if (SignalInSet(*set, guest)) ClearHold(guest); + break; + case 3: + blockedMask.store(set->bits[0] & ~UnblockableSignals); + for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].store(set->bits[word]); + if (releaseHolds) + for (int guest = 1; guest <= MaxSignal; ++guest) ClearHold(guest); + break; + } + } + } + std::atomic_thread_fence(std::memory_order_seq_cst); + DeliverUnblockedPending(); + return 0; +} + +} + extern "C" { GuestHandler APS5_VABI signal_nid_postfix(int guest, GuestHandler handler) { const auto invalid = reinterpret_cast(static_cast(-1)); @@ -601,33 +658,7 @@ int APS5_VABI sigaltstack_nid_postfix(const GuestStack* stack, GuestStack* previ return 0; } int APS5_VABI _sigprocmask_nid_postfix(int how, const GuestSignalSet* set, GuestSignalSet* previousSet) { - if (set != nullptr && (how < 1 || how > 3)) { *__error_nid_postfix() = 22; return -1; } - { - std::lock_guard lock(registration); - if (previousSet != nullptr) { - previousSet->bits[0] = blockedMask.load(); - for (int word = 1; word < 4; ++word) previousSet->bits[word] = blockedMaskExtra[word - 1].load(); - } - if (set != nullptr) { - switch (how) { - case 1: - blockedMask.fetch_or(set->bits[0] & ~UnblockableSignals); - for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_or(set->bits[word]); - break; - case 2: - blockedMask.fetch_and(~set->bits[0]); - for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_and(~set->bits[word]); - break; - case 3: - blockedMask.store(set->bits[0] & ~UnblockableSignals); - for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].store(set->bits[word]); - break; - } - } - } - std::atomic_thread_fence(std::memory_order_seq_cst); - DeliverUnblockedPending(); - return 0; + return ChangeBlockedMask(how, set, previousSet, true); } int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSet) { @@ -685,7 +716,7 @@ int APS5_VABI sigsuspend_nid_postfix(const GuestSignalSet* set) { for (int word = 1; word < 4; ++word) previous.bits[word] = blockedMaskExtra[word - 1].exchange(set->bits[word]); } WaitForDeliverableSignal(); - _sigprocmask_nid_postfix(GuestSigsetmask, &previous, nullptr); + ChangeBlockedMask(GuestSigsetmask, &previous, nullptr, false); *__error_nid_postfix() = GuestEintr; return -1; } @@ -694,9 +725,8 @@ int APS5_VABI sigpending_nid_postfix(GuestSignalSet* set) { if (set == nullptr) { *__error_nid_postfix() = 14; return -1; } auto& pending = Pending(); std::lock_guard lock(pending.lock); - set->bits[0] = pending.words[0].load(std::memory_order_relaxed) & blockedMask.load(); - for (int word = 1; word < 4; ++word) - set->bits[word] = pending.words[word].load(std::memory_order_relaxed) & blockedMaskExtra[word - 1].load(); + for (int word = 0; word < 4; ++word) + set->bits[word] = pending.words[word].load(std::memory_order_relaxed) & BlockedWord(word); return 0; } } diff --git a/core/libs/tests/GuestSignalHandlerMask.cpp b/core/libs/tests/GuestSignalHandlerMask.cpp index 855372e0c2..a9deefa3df 100644 --- a/core/libs/tests/GuestSignalHandlerMask.cpp +++ b/core/libs/tests/GuestSignalHandlerMask.cpp @@ -79,6 +79,15 @@ static void APS5_VABI RecordMask(int) { Require(sigprocmask_nid_postfix(SigBlock, nullptr, &setInHandler) == 0); } +static void APS5_VABI UnblockAndRaise(int signal) { + if (++calls == 1) { + const GuestSignalSet own{{Bit(signal), 0, 0, 0}}; + Require(sigprocmask_nid_postfix(SigUnblock, &own, nullptr) == 0); + Require(raise_nid_postfix(signal) == 0); + callsAfterNestedRaise = calls; + } +} + static void Install(int signal, void (APS5_VABI* handler)(int), int flags, std::uint32_t mask) { GuestSigaction action{reinterpret_cast(handler), flags, {{mask, 0, 0, 0}}}; Require(sigaction_nid_postfix(signal, &action, nullptr) == 0); @@ -127,6 +136,26 @@ static void CheckOtherThreadKept() { Require(CurrentMask() == 0); } +static void CheckOtherThreadKeptSameSignal() { + calls = 0; + Install(SigUsr1, BlockFromOtherThread, SaRestart, Bit(SigTerm)); + Require(raise_nid_postfix(SigUsr1) == 0); + Require(calls == 1); + Require(CurrentMask() == Bit(SigTerm)); + const GuestSignalSet set{{Bit(SigTerm), 0, 0, 0}}; + Require(sigprocmask_nid_postfix(SigUnblock, &set, nullptr) == 0); + Require(CurrentMask() == 0); +} + +static void CheckUnblockInsideHandler() { + calls = 0; + callsAfterNestedRaise = 0; + Install(SigUsr2, UnblockAndRaise, SaRestart, 0); + Require(raise_nid_postfix(SigUsr2) == 0); + Require(callsAfterNestedRaise == 2); + Require(CurrentMask() == 0); +} + static void CheckUpperMaskWords() { calls = 0; const GuestSigaction action{reinterpret_cast(RecordMask), SaRestart, {{Bit(SigKill), 1u << 7, 0, 1u << 31}}}; @@ -158,6 +187,8 @@ int main() { CheckNodefer(); CheckResethand(); CheckOtherThreadKept(); + CheckOtherThreadKeptSameSignal(); + CheckUnblockInsideHandler(); CheckUpperMaskWords(); CheckLeftWithLongjmp(); return 0; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 2d9867a247..6408643d4d 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -212,7 +212,7 @@ - [sigtimedwait](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) waits on a process-wide pending set instead of throwing: a signal raised while blocked stays pending and is taken by `sigwait`, `sigwaitinfo` or `sigtimedwait`, delivered to the handler when `_sigprocmask` unblocks it, or delivered by `pause`. The set covers all 128 signals and the lowest number that is pending and in the set is selected (FreeBSD specifies the lowest number only among real-time signals). Selection, `EAGAIN` on timeout, `EINVAL` for a `tv_nsec` outside 0 to 1000000000 while nothing is pending and `NULL` timeout blocking indefinitely follow FreeBSD's [sigtimedwait(2)](https://man.freebsd.org/cgi/man.cgi?query=sigtimedwait). `sigwait` returns 0 with the number in its output, as POSIX specifies, and `sigwaitinfo` and `sigtimedwait` return the number, as FreeBSD does. The guest `siginfo_t` follows FreeBSD 11's `sys/sys/siginfo.h`: 80 bytes, `si_signo` at 0, `si_code` at 8, `si_pid` at 12, `si_uid` at 16, `si_addr` at 24, `si_value` at 32 and `si_band` at 40; only `si_signo`, `si_code` (`SI_USER`) and `si_pid` (the guest pid) are written, `si_uid` stays 0 because no uid is modelled, and the console's layout is not verified. PPSA85300 imports `sigtimedwait` and fails to load without it; its only call site is VLC's `vlc_writev` ([filesystem.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/posix/filesystem.c#L243-L278)), which polls with a zero timeout to drain a `SIGPIPE` only after `writev` fails with `EPIPE`. The waits are not cancellation points and an unblocked caught signal does not interrupt them with `EINTR`, where FreeBSD's `sigwaitinfo` and `sigtimedwait` may return it. `SIGPIPE` has no host equivalent and is never raised by a host write here (see `read, write and vector I/O`), so VLC's drain finds nothing - [pause, sigsuspend, sigwait, sigwaitinfo](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) take the pending set of `sigtimedwait` instead of throwing. `pause` waits for a pending signal that is not blocked and whose disposition can be delivered, runs its handler on the calling thread and returns -1 with `EINTR`; an ignored signal is discarded and the wait continues, and it never returns for a signal whose handler cannot run. A signal whose registered handler has `SA_SIGINFO` still throws, and one whose disposition is the default action and that has no host equivalent (every signal except 2, 4, 6, 8, 11 and 15) is neither delivered nor discarded: it stays pending until a wait takes it, where FreeBSD runs the default action, which terminates the process for most of them. A pending `SIGALRM` with the default disposition exits the process with status 142 once it is unblocked, as FreeBSD's default action does. PPSA68091, PPSA99169, PPSA99420 and PPSA97358 import them and fail to load without them. PPSA68091 calls `pause` at one site (0x8F7266), in a loop that waits while a global holds another thread's `errno` address. PPSA99169 only stores the address of `pause` in a table of symbol addresses (0x30DB1). PPSA99420 calls `pause` at 0x3BD80F6 and 0x3C9E220, `sigwait` at 0x3C9E4C7 and `sigwaitinfo` at 0x3C9E588. PPSA97358 calls `sigwait` from QEMU's `sigwait_compat` thread ([compatfd.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/compatfd.c#L28-L53)), which `qemu_signal_init` ([main-loop.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/main-loop.c#L110-L147)) starts for `SIGIO`, `SIGALRM` and `SIGBUS` when the main loop is created, so it reaches the wait at start-up, and from the vCPU thread of the dummy accelerator ([dummy-cpus.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/accel/dummy-cpus.c#L48-L56)). `sigsuspend` swaps the process-wide mask of `_sigprocmask` for its argument, waits as `pause` does through the same helper, then restores the mask through `_sigprocmask`, and returns -1 with `EINTR`; a null set throws, as the console faults in libthr instead of returning `EFAULT`. All four words of the argument replace the mask and are restored afterwards, as in `_sigprocmask`. Java 25 LAN / DNS (PPSA99869, #5501) imports it and fails to load without it, after `sysctlnametomib`, `fegetenv` and `mincore`. None of the five waits is a cancellation point - [getsid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns the calling process ID for pid 0 and the calling process, and `ESRCH` for other process IDs, assuming the single guest process is its own session leader. PPSA74126 imports it and fails to load without it -- [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do. While a handler runs, its own signal (unless `SA_NODEFER`) and the signals of its mask other than `SIGKILL` and `SIGSTOP` are added to the process-wide mask, and `SA_RESETHAND` resets the disposition to the default before it runs, as FreeBSD's `postsig_done` does. When the handler returns, only the signals it added are unblocked again, so a mask change another thread made meanwhile is kept, but so is one the handler made itself, which FreeBSD's `sigreturn` would undo. A handler left with `longjmp`, or with `siglongjmp` to a buffer that saved the mask, gets back the mask that buffer saved, as on FreeBSD; a signal raised meanwhile stays pending and is delivered when the handler returns to `raise`, but one left pending by a handler that a host signal ran asynchronously waits for the next `raise` or `_sigprocmask`. `SA_ONSTACK` is not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating +- [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do. While a handler runs, its own signal (unless `SA_NODEFER`) and the signals of its mask other than `SIGKILL` and `SIGSTOP` are blocked, and `SA_RESETHAND` resets the disposition to the default before it runs, as FreeBSD's `postsig_done` does. Those blocks are held apart from the process-wide mask that `sigprocmask` sets, which reports them, and are released when the handler returns, so a block another thread adds meanwhile is kept, for the same signal too, and so is one the handler adds itself, which FreeBSD's `sigreturn` would undo. `SIG_UNBLOCK` of a held signal releases its hold and `SIG_SETMASK` releases them all; a `sigsuspend` inside a handler keeps them, where FreeBSD replaces the whole thread mask. A handler left with `longjmp`, or with `siglongjmp` to a buffer that saved the mask, gets back the mask that buffer saved, as on FreeBSD; a signal raised meanwhile stays pending and is delivered when the handler returns to `raise`, but one left pending by a handler that a host signal ran asynchronously waits for the next `raise` or `_sigprocmask`. `SA_ONSTACK` is not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating - [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console - [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console From d80576212098755dd09e02b1832865b53f930899 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:09:42 -0300 Subject: [PATCH 058/428] feat(libSceNet): support SO_KEEPALIVE --- core/libs/prx/libSceNet/CMakeLists.txt | 11 ++++ core/libs/prx/libSceNet/Export.cpp | 20 ++++++ core/libs/tests/GuestNetKeepalive.cpp | 90 ++++++++++++++++++++++++++ docs/dev/debt/libSceNet.md | 2 + 4 files changed, 123 insertions(+) create mode 100644 core/libs/tests/GuestNetKeepalive.cpp diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index 95faf4584b..857fcbcb3c 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -31,6 +31,17 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test_executable(guest_net_keepalive_tests ${LIBS_INCLUDE_DIR}/tests/GuestNetKeepalive.cpp) + target_include_directories(guest_net_keepalive_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_net_keepalive_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(guest_net_keepalive_tests) + add_test(NAME guest_sce_net_keepalive COMMAND guest_net_keepalive_tests) + set_tests_properties(guest_sce_net_keepalive PROPERTIES TIMEOUT 20) + if(WIN32) + target_link_libraries(guest_net_keepalive_tests PRIVATE ws2_32) + set_tests_properties(guest_sce_net_keepalive PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_datagram_receive_tests ${LIBS_INCLUDE_DIR}/tests/GuestDatagramReceive.cpp) target_include_directories(guest_datagram_receive_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_datagram_receive_tests PRIVATE libSceNet libkernel libc) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 808cdc7364..925a5574f7 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -275,6 +275,7 @@ constexpr int NET_SO_RCVTIMEO = 0x1006; constexpr int NET_SO_ERROR = 0x1007; constexpr int NET_SO_NBIO = 0x1200; constexpr int NET_SO_REUSEADDR = 0x4; +constexpr int NET_SO_KEEPALIVE = 0x8; constexpr int NET_SO_BROADCAST = 0x20; constexpr int NET_SO_ONESBCAST = 0x10000; constexpr int NET_SO_SNDBUF = 0x1001; @@ -399,6 +400,7 @@ struct Sock { int snd_buffer = 0; int rcv_buffer = 0; int onesbcast = 0; + int keepalive = 0; }; #ifdef _WIN32 @@ -1510,6 +1512,11 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval const int native_option = optname == NET_SO_REUSEADDR ? SO_REUSEADDR : optname == NET_SO_BROADCAST ? SO_BROADCAST : optname == NET_SO_SNDBUF ? SO_SNDBUF : SO_RCVBUF; if (::setsockopt(socket.native->value, SOL_SOCKET, native_option, reinterpret_cast(&value), sizeof(value)) != 0) return fail(native_error()); + } else if (optname == NET_SO_KEEPALIVE) { + const int enabled = value != 0; + if (socket.type == NET_SOCK_STREAM && + ::setsockopt(socket.native->value, SOL_SOCKET, SO_KEEPALIVE, reinterpret_cast(&enabled), sizeof(enabled)) != 0) + return fail(native_error()); } else if (optname == NET_SO_ONESBCAST) { } else { return fail(NET_EOPNOTSUPP); @@ -1523,6 +1530,7 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval if (optname == NET_SO_SNDBUF) it->second.snd_buffer = value; if (optname == NET_SO_RCVBUF) it->second.rcv_buffer = value; if (optname == NET_SO_ONESBCAST) it->second.onesbcast = value; + if (optname == NET_SO_KEEPALIVE) it->second.keepalive = value != 0 ? NET_SO_KEEPALIVE : 0; } return 0; } @@ -1543,6 +1551,18 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint std::memcpy(optval, &timeout, *optlen); return 0; } + if (level == NET_SOL_SOCKET && optname == NET_SO_KEEPALIVE) { + int enabled = socket.keepalive; + if (socket.type == NET_SOCK_STREAM) { + NativeLength size = sizeof(enabled); + if (::getsockopt(socket.native->value, SOL_SOCKET, SO_KEEPALIVE, reinterpret_cast(&enabled), &size) != 0) + return fail(native_error()); + } + const int value = enabled != 0 ? NET_SO_KEEPALIVE : 0; + *optlen = std::min(*optlen, static_cast(sizeof(value))); + std::memcpy(optval, &value, *optlen); + return 0; + } if (*optlen < sizeof(int)) return fail(NET_EINVAL); if (level == NET_IPPROTO_TCP && socket.type != NET_SOCK_STREAM) return fail(NET_EINVAL); if (level == NET_IPPROTO_TCP && optname == NET_TCP_NODELAY) { diff --git a/core/libs/tests/GuestNetKeepalive.cpp b/core/libs/tests/GuestNetKeepalive.cpp new file mode 100644 index 0000000000..e84e0e8244 --- /dev/null +++ b/core/libs/tests/GuestNetKeepalive.cpp @@ -0,0 +1,90 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/Socket/include/SocketPoll.hpp" + +extern "C" { +int APS5_VABI sceNetInit_nid_postfix(); +int APS5_VABI sceNetSocket(const char*, int, int, int); +int APS5_VABI sceNetSocketClose(int); +int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); +int APS5_VABI sceNetGetsockopt(int, int, int, void*, std::uint32_t*); +int* APS5_VABI sceNetErrnoLoc(); +int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); +} + +static void Require(bool condition) { + if (!condition) std::abort(); +} + +static bool Failed(std::int64_t result, int error) { + return result == static_cast(0x80410100u | static_cast(error)) && *sceNetErrnoLoc() == error; +} + +static void CheckKeepalive() { + for (const int family : {2, 28}) { + for (const int type : {1, 2}) { + const int socket = sceNetSocket("keepalive", family, type, type == 1 ? 6 : 17); + Require(socket >= 0); + int value = -1; + std::uint32_t size = sizeof(value); + Require(sceNetGetsockopt(socket, 0xffff, 8, &value, &size) == 0 && value == 0 && size == sizeof(value)); + for (const int setting : {1, -1, 0, 8, 0}) { + Require(sceNetSetsockopt(socket, 0xffff, 8, &setting, sizeof(setting)) == 0); + size = sizeof(value); + Require(sceNetGetsockopt(socket, 0xffff, 8, &value, &size) == 0 && value == (setting ? 8 : 0)); + if (type == 1) { + std::shared_ptr owner; + std::uintptr_t native = 0; + Require(KernelFindSocketNative_nid_no_patch(socket, &owner, &native)); + int enabled = 0; +#ifdef _WIN32 + int length = sizeof(enabled); +#else + socklen_t length = sizeof(enabled); +#endif + Require(::getsockopt(native, SOL_SOCKET, SO_KEEPALIVE, reinterpret_cast(&enabled), &length) == 0); + Require((enabled != 0) == (setting != 0)); + } + } + const int enabled = 1; + Require(Failed(sceNetSetsockopt(socket, 0xffff, 8, &enabled, sizeof(enabled) - 1), 22)); + Require(Failed(sceNetSetsockopt(socket, 0xffff, 8, nullptr, sizeof(enabled)), 22)); + Require(setsockopt_nid_postfix(socket, 0xffff, 8, &enabled, sizeof(enabled)) == 0); + const int other = sceNetSocket("keepalive-other", family, type, type == 1 ? 6 : 17); + Require(other >= 0); + size = sizeof(value); + Require(sceNetGetsockopt(other, 0xffff, 8, &value, &size) == 0 && value == 0); + Require(sceNetSocketClose(other) == 0); + Require(Failed(sceNetGetsockopt(socket, 0xffff, 8, nullptr, &size), 22)); + Require(Failed(sceNetGetsockopt(socket, 0xffff, 8, &value, nullptr), 22)); + for (const std::uint32_t length : {0u, 1u, 3u, 4u, 8u}) { + std::array bytes; + bytes.fill(0xa5); + size = length; + const int expected = 8; + Require(sceNetGetsockopt(socket, 0xffff, 8, bytes.data(), &size) == 0 && size == std::min(length, 4u)); + Require(std::memcmp(bytes.data(), &expected, size) == 0); + Require(std::all_of(bytes.begin() + size, bytes.end(), [](auto byte) { return byte == 0xa5; })); + } + Require(sceNetSocketClose(socket) == 0); + size = sizeof(value); + Require(Failed(sceNetGetsockopt(socket, 0xffff, 8, &value, &size), 9)); + Require(Failed(sceNetSetsockopt(socket, 0xffff, 8, &enabled, sizeof(enabled)), 9)); + } + } +} + +int main() { + Require(sceNetInit_nid_postfix() == 0); + CheckKeepalive(); +} diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index f10d1eb873..25b4668d43 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -24,6 +24,8 @@ - [sceNetConfigRoutingStart](../../../core/libs/prx/libSceNet/Export.cpp): five pointer/scalar arguments are confirmed. Input address data is eight bytes; the mutable server parameter is 20 bytes and duplicates its first address when its count is one. Optional rules contain a count and pointer to 20-byte records with protocol, two address/port pairs and flags. Layouts are defined; rule flag meanings and protocol-specific port interpretation remain unresolved. - [sceNetConfigRoutingShowRoutingConfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the native routine consumes no arguments; the throwing implementation remains unchanged. +- [SO_KEEPALIVE](../../../core/libs/prx/libSceNet/Export.cpp) uses the host TCP keepalive timers and probe count, which can differ from the console. The boolean flag, bit-valued readback and truncated output follow FreeBSD 11.4's [sosetopt and sogetopt](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/kern/uipc_socket.c), without PS5 hardware verification. Non-stream sockets retain the flag without sending probes, as FreeBSD does. Null pointers retain the existing libSceNet `EINVAL` result instead of FreeBSD's `EFAULT`. + ### Functional - [sceNetResolverStartAton6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reverse-resolves a 16-byte IPv6 address through the host resolver (`getnameinfo` with `NI_NAMEREQD`) into the caller's buffer of `len` bytes, as `sceNetResolverStartAton` does for IPv4. The `(rid, addr, hostname, len, timeout, retry, flags)` signature comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L992-L997). An address with no name, or a name longer than `len - 1` bytes, fails with `RESOLVER_ENODNS` (`0x804101E1`) and leaves the buffer untouched; whether the console truncates instead is not verified. Abort flag 2 (ATON preservation) fails the next call with `EINTR` as it does `sceNetResolverStartAton`. Timeout and retry are ignored, and any nonzero flags throw because their meaning is unknown From f70cbc62b55ee791b7647d85b4615a839334e2bd Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:18:53 -0300 Subject: [PATCH 059/428] test(libSceNet): check keepalive inheritance --- core/libs/tests/GuestNetKeepalive.cpp | 38 +++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/core/libs/tests/GuestNetKeepalive.cpp b/core/libs/tests/GuestNetKeepalive.cpp index e84e0e8244..d8071ab65f 100644 --- a/core/libs/tests/GuestNetKeepalive.cpp +++ b/core/libs/tests/GuestNetKeepalive.cpp @@ -16,6 +16,11 @@ extern "C" { int APS5_VABI sceNetInit_nid_postfix(); int APS5_VABI sceNetSocket(const char*, int, int, int); int APS5_VABI sceNetSocketClose(int); +int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI sceNetGetsockname(int, void*, std::uint32_t*); +int APS5_VABI sceNetListen(int, int); +int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); +int APS5_VABI sceNetAccept(int, void*, std::uint32_t*); int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); int APS5_VABI sceNetGetsockopt(int, int, int, void*, std::uint32_t*); int* APS5_VABI sceNetErrnoLoc(); @@ -87,4 +92,37 @@ static void CheckKeepalive() { int main() { Require(sceNetInit_nid_postfix() == 0); CheckKeepalive(); + const auto readFlag = [](int socket) { + int value = -1; + std::uint32_t size = sizeof(value); + Require(sceNetGetsockopt(socket, 0xffff, 8, &value, &size) == 0 && size == sizeof(value)); + return value; + }; + for (const int family : {2, 28}) { + const int listener = sceNetSocket("keepalive-listener", family, 1, 6); + const int client = sceNetSocket("keepalive-client", family, 1, 6); + Require(listener >= 0 && client >= 0); + const int on = 1; + const int off = 0; + Require(sceNetSetsockopt(listener, 0xffff, 8, &on, sizeof(on)) == 0); + std::array address{}; + std::uint32_t size = family == 2 ? 16 : 28; + address[0] = size; + address[1] = family; + if (family == 2) { + address[4] = 127; + address[7] = 1; + } else { + address[23] = 1; + } + Require(sceNetBind_nid_postfix(listener, address.data(), size) == 0); + Require(sceNetListen(listener, 1) == 0); + Require(sceNetGetsockname(listener, address.data(), &size) == 0); + Require(sceNetConnect(client, address.data(), size) == 0); + const int accepted = sceNetAccept(listener, nullptr, nullptr); + Require(accepted >= 0 && readFlag(accepted) == 8 && readFlag(client) == 0); + Require(sceNetSetsockopt(listener, 0xffff, 8, &off, sizeof(off)) == 0); + Require(readFlag(listener) == 0 && readFlag(accepted) == 8); + for (const int socket : {accepted, client, listener}) Require(sceNetSocketClose(socket) == 0); + } } From a2e0bf19e62f15dd060745849ad67a7c439bd22d Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:53:39 -0700 Subject: [PATCH 060/428] perf(libc): count address-space queries only in the write-watch test The VirtualQuery counter that windows_write_watch_tests reads is now compiled only into that test, through APS5_COUNT_MAPPING_QUERIES, so the runtime's WindowsMappings::query does no atomic update per query. --- core/libs/CMakeLists.txt | 2 +- core/libs/prx/libc/include/WindowsMappings.hpp | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index c7ef053323..8024f9c930 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -317,7 +317,7 @@ if(BUILD_TESTING) if(WIN32) add_test_executable(windows_write_watch_tests tests/WindowsWriteWatch.cpp) target_include_directories(windows_write_watch_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_compile_definitions(windows_write_watch_tests PRIVATE _WIN32_WINNT=0x0A00) + target_compile_definitions(windows_write_watch_tests PRIVATE _WIN32_WINNT=0x0A00 APS5_COUNT_MAPPING_QUERIES=1) if(MINGW) target_compile_options(windows_write_watch_tests PRIVATE -fasynchronous-unwind-tables) endif() diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index f394e187d1..3a9f2cefac 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -5,7 +5,9 @@ #include #include #include +#ifdef APS5_COUNT_MAPPING_QUERIES #include +#endif #include #include #include @@ -29,9 +31,11 @@ public: return *mappings; } +#ifdef APS5_COUNT_MAPPING_QUERIES static std::uint64_t MemoryQueries() { return queries.load(std::memory_order_relaxed); } +#endif void* Reserve(void* address, std::size_t bytes) { return allocate(GetCurrentProcess(), address, bytes, MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, nullptr, 0); @@ -980,7 +984,9 @@ private: static MEMORY_BASIC_INFORMATION query(std::uintptr_t address) { MEMORY_BASIC_INFORMATION memory{}; +#ifdef APS5_COUNT_MAPPING_QUERIES queries.fetch_add(1, std::memory_order_relaxed); +#endif if (VirtualQuery(reinterpret_cast(address), &memory, sizeof(memory)) != sizeof(memory)) fail("query guest memory"); return memory; } @@ -1116,7 +1122,9 @@ private: std::map splitAliases; std::size_t granularity = 0x10000; bool watchResident = false; +#ifdef APS5_COUNT_MAPPING_QUERIES static inline std::atomic queries{0}; +#endif std::mutex mutex; AllocateFunction allocate = nullptr; MapFunction map = nullptr; From 6b2be773aad29ccf33fb9881ef456f704b3fca80 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Thu, 22 Oct 2026 03:01:05 +0200 Subject: [PATCH 061/428] docs(debt): say "several titles" in the storage image refusal entry A fourth title reached the refusal on another GPU and OS (reported on the PR), so the entry no longer names a count. Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceAgcDriver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index cacb31d7a5..36d6af8aec 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -106,7 +106,7 @@ - [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. Explicit-size file writes (`write`, `pwrite`, `writev`, `pwritev`, AIO and `fwrite`) synchronize their source ranges through the [host-read hook](../../../core/libs/prx/libc/src/GuestArena.cpp); other host readers are not covered. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default. - [`APS5_SKIP_REFUSED_DRAWS`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp) (libSceAgcDriver) drops a draw whose graphics stage the recompiler refuses instead of failing the queue, so the frame lacks that draw, and the stage is compiled and refused again at each such draw. Compute refusals still fail. - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending saves its bytes through the unwatch hook, which also runs when a failed collect takes a range out of the watch. A range that leaves the watch without the hook running over it (an import fallback that unwatched without saving) leaves its pending results without saved bytes; storing such results throws. A unit whose results were stored while its blocks were watched keeps only a generation; when its blocks leave the watch while nothing is pending, the hook saves nothing, and results that land in it afterwards have neither saved bytes nor a watch, so storing them throws too. -- [Storage images over uncommitted guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) - a storage image whose guest range has no committed page when it is created throws in the `StorageTexture` constructor, which aborts the guest. Three titles reach this intermittently in runs of a few minutes, on AMD and NVIDIA alike; in two of the three logs, lines about storage images over released or rewritten memory (`memory no longer registered`, `guest memory changed while GPU results were pending`) come shortly before it. The log does not name the image's address. What the console does with a storage descriptor over memory that is not committed is unknown, and which allocation the guest recreates there was not found. +- [Storage images over uncommitted guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) - a storage image whose guest range has no committed page when it is created throws in the `StorageTexture` constructor, which aborts the guest. Several titles reach this intermittently in runs of a few minutes, on AMD and NVIDIA alike; in two of the three logs, lines about storage images over released or rewritten memory (`memory no longer registered`, `guest memory changed while GPU results were pending`) come shortly before it. The log does not name the image's address. What the console does with a storage descriptor over memory that is not committed is unknown, and which allocation the guest recreates there was not found. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory when the import probe keeps the write watch and a settle queue exists: those of 1 MiB or more stay watched, and smaller ones return to the watch once their settle has completed, unless a storage image overlaps them (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. - [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. On Windows, the NVIDIA driver (617.42) marks every page of a host import of private `MEM_WRITE_WATCH` memory written, asynchronously, until a GPU use of the import completes; a 4-byte read of it on the settle queue ends that even while main-queue work that used the import first is still running, and an earlier import of the same pages whose first use runs after that read marks none of them (`agc_import_settle_first_use`, `agc_import_settle_earlier_import`). Such an import stays watched only after a 4-byte read of it on the settle queue has completed inside the window. Each one waits for that read, which keeps its window open for one round trip on the settle queue, so a same-value CPU store from another thread in that time counts as unchanged too. An import made where no settle queue exists is compared instead. An import of under 1 MiB is not settled inside its window, because the settle blocks the importing thread and a title that imports a 64 KiB buffer around every submission lost about 19 % of its frame rate to that wait (Dripl1x, #6060). It is compared until its read, submitted in a batch by a later import lookup under `GpuMutex` while the imports match the registry, has completed; a later lookup then returns its whole blocks to the write watch, each stamped as a CPU store, so whatever was derived from them while they were compared reads as changed once. Its blocks stay compared while storage results are pending over them, and those of an import retired before that stay compared. - [Settled dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a dma-buf import of 1 MiB or more is read by a 4-byte copy on the settle queue right after it is made (`settleImport`), so the kernel maps its pages for the GPU there and not in the first submission that uses it (about 2 s for a 3 GiB import on RADV). The import's buffer is shared with the settle queue's family. An import made at map time settles on the guest thread that maps the range, inside its map call, so that thread waits while the kernel maps the pages: 0.47 to 2.2 s for the 3 GiB range of Hades (PPSA03355) and 0.49 to 4.3 s for the 4.5 GiB range of ASTRO BOT (PPSA21567) on RADV. Both titles map these ranges at start-up; a title that maps large ranges during gameplay stalls its mapping thread for as long. An import made from a submission settles under the GPU mutex, so its frame still pays for it; a smaller import, and every import on a device with no settle queue (one without a copy-capable queue besides the main one), pays in its first submission. A range imported through a host pointer, as lavapipe imports shared memory, is not settled From a00adaa8c2ba9b235d2ae66e58045da6df60293f Mon Sep 17 00:00:00 2001 From: fablevoid <340827264+fable-void@users.noreply.github.com> Date: Wed, 14 Oct 2026 07:48:49 +0000 Subject: [PATCH 062/428] fix(agc): find a template's data bindings by binding number A resource template keeps no binding of its own for an image table: the build hands the shader's image table binding to the table stage. Its binding list is therefore one shorter than the shader's, yet DataWordsDiffer and refreshData paired the two lists by index and required equal lengths. Every data refresh of a compute template whose shader reads an image table threw "template bindings disagree with the shader", and DataWordsDiffer reported different words for the shader the template was built from. Evidence: a title stopped once its pipelines came from the cache with "compute shader 0x80003c7100: AGC graphics: template bindings disagree with the shader". The shader listed 9 bindings (guest buffers, three guest image arrays, the fault buffer, the flattened SRT, the sampler table, the image table map and the image table at 260); the reused template held 8, without binding 260. Both functions now look up the template binding of each data binding by its binding number. Tested: agc_driver_graphics builds a compute template from an image table map, an empty image table and shader data, checks that its own words compare equal, refreshes it with other words, and checks the data buffer and the compare afterwards. Without the change it fails with "a template's own words differ from the shader that built it". Cherry-picked from fable-void's 7bb699b85c; conflict in tests/Graphics.cpp resolved to main's image table ABI: the test's header no longer sets HeaderImageSlots, a word main's ABI does not have (main keeps HeaderElementBase at that index), and its mock context gets the image table limits main's imageTableAllocationTests sets (image table capacity and sampled images per set and per stage, 2 each), since main refuses image tables on a context without them. --- .../Graphics/src/ShaderResources.cpp | 21 ++++++----- .../prx/libSceAgcDriver/tests/Graphics.cpp | 36 +++++++++++++++++++ 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index d6d84cd4aa..34921457fd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -4304,12 +4304,11 @@ std::uint64_t ShaderResources::DataWordsHash(const CompiledShader& shader) { bool ShaderResources::DataWordsDiffer(const CompiledShader& shader) const { Require(shader.program != nullptr, "missing compiled shader"); const auto& program = *shader.program; - if (program.bindings.size() != bindings.size()) return true; - for (std::size_t index = 0; index < program.bindings.size(); ++index) { - const auto& binding = program.bindings[index]; + for (const auto& binding : program.bindings) { if (!DataRole(binding.role)) continue; - if (bindings[index].allocations.size() != 1) return true; - const auto& allocation = allocations[bindings[index].allocations.front()]; + const auto held = std::ranges::find_if(bindings, [&](const Binding& candidate) { return candidate.layout.binding == binding.binding; }); + if (held == bindings.end() || held->allocations.size() != 1) return true; + const auto& allocation = allocations[held->allocations.front()]; if (allocation.dataWords.size() != binding.guestDescriptor.size() || !std::equal(allocation.dataWords.begin(), allocation.dataWords.end(), binding.guestDescriptor.begin())) return true; } return false; @@ -4324,23 +4323,23 @@ bool ShaderResources::RefreshData(VkCommandBuffer commands, const CompiledShader bool ShaderResources::refreshData(VkCommandBuffer commands, const CompiledShader& shader, Recorder* recorder) { Require(shader.program != nullptr, "missing compiled shader"); const auto& program = *shader.program; - Require(program.bindings.size() == bindings.size(), "template bindings disagree with the shader"); bool recorded = false; // One [gputime] range of class TemplateDataRefresh around the updates, begun at the first one // (a refresh that finds every word equal records nothing and times nothing). auto timing = Recorder::NoTiming; std::uint64_t refreshedBytes = 0; - for (std::size_t index = 0; index < program.bindings.size(); ++index) { - const auto& binding = program.bindings[index]; + for (const auto& binding : program.bindings) { if (!DataRole(binding.role)) continue; - Require(bindings[index].allocations.size() == 1, "data binding without its buffer"); - auto& allocation = allocations[bindings[index].allocations.front()]; + const auto held = std::ranges::find_if(bindings, [&](const Binding& candidate) { return candidate.layout.binding == binding.binding; }); + Require(held != bindings.end(), "template bindings disagree with the shader"); + Require(held->allocations.size() == 1, "data binding without its buffer"); + auto& allocation = allocations[held->allocations.front()]; const auto size = binding.guestDescriptor.size() * sizeof(std::uint32_t); Require(allocation.buffer != nullptr && !allocation.guest && allocation.size == size && size <= MaxRefreshBytes, "template data buffer cannot take the dispatch's words"); if (allocation.dataWords.size() == binding.guestDescriptor.size() && std::equal(allocation.dataWords.begin(), allocation.dataWords.end(), binding.guestDescriptor.begin())) continue; if (binding.role == ShaderRecompiler::DescriptorRole::ShaderData) ValidateRuntimeResources(shader, binding.guestDescriptor); if (recorder != nullptr && timing == Recorder::NoTiming) timing = recorder->BeginGpuTiming(Recorder::CommandClass::TemplateDataRefresh); - writeDataWords(commands, bindings[index].allocations.front(), binding.guestDescriptor); + writeDataWords(commands, held->allocations.front(), binding.guestDescriptor); allocation.dataWords.assign(binding.guestDescriptor.begin(), binding.guestDescriptor.end()); refreshedBytes += size; recorded = true; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 428c1ce75e..45905104f0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -6773,6 +6773,41 @@ void misalignedShaderDataTests() { Require(mock.live == 0, "misaligned shader data resources leaked Vulkan objects"); } +void imageTableTemplateRefreshTests() { + namespace Abi = ShaderRecompiler::ImageTableAbi; + mock = MockVulkan{}; + auto context = mockContext(); + context.imageTableCapacity = 2; + context.limits.maxDescriptorSetSampledImages = 2; + context.limits.maxPerStageDescriptorSampledImages = 2; + const auto commands = reinterpret_cast(std::uintptr_t{1}); + { + ShaderRecompiler::RecompileResult compute; + auto map = makeBinding(Role::ImageTableMap, static_cast(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap), 1, std::vector(Abi::HeaderWords)); + map.guestDescriptor[Abi::HeaderVersion] = Abi::Version; + map.guestDescriptor[Abi::HeaderElementFaults] = Abi::HeaderWords; + map.guestDescriptor[Abi::HeaderSamplerFaults] = Abi::HeaderWords; + compute.bindings.push_back(map); + auto images = makeBinding(Role::ImageTable, ShaderRecompiler::RuntimeAbi::ImageTableBinding, 0, {}); + images.kind = Kind::SampledImage; + compute.bindings.push_back(images); + compute.bindings.push_back(makeBinding(Role::ShaderData, 1, 1, ShaderDataWords({0x11, 0x22}))); + compute.shaderDataDwords = ShaderRecompiler::RuntimeAbi::ShaderDataDwords; + auto live = compute; + live.bindings[2].guestDescriptor = ShaderDataWords({0x33, 0x44}); + const AgcDriver::Graphics::CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &compute, 0}; + const AgcDriver::Graphics::CompiledShader liveShader{ShaderRecompiler::ShaderStage::Compute, &live, 0}; + AgcDriver::Graphics::ShaderResources resources(context, shader); + const auto data = findWrite(1).buffers.at(0).buffer; + Require(!resources.DataWordsDiffer(shader), "a template's own words differ from the shader that built it"); + Require(resources.DataWordsDiffer(liveShader), "a template with an image table missed the dispatch's new words"); + Require(resources.RefreshData(commands, liveShader), "a refresh of a template with an image table recorded nothing"); + Require(sameBytes(bufferBytes(data), live.bindings[2].guestDescriptor.data(), 3u * sizeof(std::uint32_t)), "a refresh of a template with an image table did not write the dispatch's words"); + Require(!resources.DataWordsDiffer(liveShader), "the refreshed template's words are not the dispatch's"); + } + Require(mock.live == 0, "image table template refresh resources leaked Vulkan objects"); +} + struct ModuleShape { bool fragment = false; std::optional fragmentMode; @@ -11210,6 +11245,7 @@ int main(int argc, char** argv) { samplerTableDeviceLimitTests(); tableUpdateAfterBindTests(); misalignedShaderDataTests(); + imageTableTemplateRefreshTests(); debugBranchTests(); hostImportBudgetTests(); textureCacheBudgetTests(); From 55e93456b22e15994082cca9b9424d27940cef4b Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:28:06 +0000 Subject: [PATCH 063/428] perf(agc): count template hits whose data bindings pair by number With APS5_PROFILE_DRAW set, the [rescache] line now counts the template hits that took a data refresh while the template held fewer bindings than the shader (no image table binding of its own): the refreshes that threw "template bindings disagree with the shader" before the bindings were paired by binding number. --- core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp | 5 +++-- .../prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 45a1354a4b..c14b927721 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -4157,7 +4157,7 @@ struct DispatchCounters { std::array phaseTotals{}; std::uint64_t profiledDispatches = 0; IndirectHold indirectHold; - std::uint64_t cacheHits = 0, cacheMisses = 0, cacheInvalidated = 0, templateRefreshed = 0, templateSameWords = 0; + std::uint64_t cacheHits = 0, cacheMisses = 0, cacheInvalidated = 0, templateRefreshed = 0, templateSameWords = 0, templateByNumber = 0; double templateRevalidateMs = 0; std::uint64_t procLookups = 0, preBarriersRecorded = 0, preBarriersSkipped = 0; std::chrono::steady_clock::time_point cacheReport = std::chrono::steady_clock::now(); @@ -4799,6 +4799,7 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { // after an earlier dispatch's reads of the buffers by that dispatch's trailing barrier. A // recipe compares the two 64-bit hashes first: equal hashes mean the buffers hold the words. const bool differs = record.dataRefresh == RecordedDispatch::DataRefresh::Words || resources.DataWordsHash() != record.dataWordsHash; + if (timer.profile && record.shader->program != nullptr && record.shader->program->bindings.size() != resources.BindingCount()) ++d.templateByNumber; if (differs && (record.resourceKeyMatched ? resources.refreshData(commands, *record.shader, &recorder) : resources.RefreshData(commands, *record.shader, &recorder))) { covered = 0; ++d.templateRefreshed; @@ -5299,7 +5300,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec } if (profile && std::chrono::steady_clock::now() - d.cacheReport > std::chrono::seconds(10)) { d.cacheReport = std::chrono::steady_clock::now(); - AgcDriver::ProfilePrint_nid_no_patch("[rescache] (cumulative): %llu hits, %llu misses, %llu invalidated, %zu entries (dispatch + draw); template hits: %llu refreshed the data buffers (revalidate %.1f ms), %llu had the same words; Find calls %llu, Touch calls %llu\n", static_cast(d.cacheHits), static_cast(d.cacheMisses), static_cast(d.cacheInvalidated), state->resourceCache.Size(), static_cast(d.templateRefreshed), d.templateRevalidateMs, static_cast(d.templateSameWords), static_cast(ResourceCache::Finds()), static_cast(ResourceCache::Touches())); + AgcDriver::ProfilePrint_nid_no_patch("[rescache] (cumulative): %llu hits, %llu misses, %llu invalidated, %zu entries (dispatch + draw); template hits: %llu refreshed the data buffers (revalidate %.1f ms), %llu had the same words, %llu paired their data bindings by number (no image table binding of their own); Find calls %llu, Touch calls %llu\n", static_cast(d.cacheHits), static_cast(d.cacheMisses), static_cast(d.cacheInvalidated), state->resourceCache.Size(), static_cast(d.templateRefreshed), d.templateRevalidateMs, static_cast(d.templateSameWords), static_cast(d.templateByNumber), static_cast(ResourceCache::Finds()), static_cast(ResourceCache::Touches())); AgcDriver::ProfilePrint_nid_no_patch("[vk] deviceProc lookups inside the device call (cumulative): %llu (%.2f per dispatch); pre-dispatch barriers recorded %llu, skipped %llu; compute pipelines created %llu (%llu under the GPU mutex)\n", static_cast(d.procLookups), d.profiledDispatches != 0 ? static_cast(d.procLookups) / static_cast(d.profiledDispatches) : 0.0, static_cast(d.preBarriersRecorded), static_cast(d.preBarriersSkipped), static_cast(computePipelinesCreated.load(std::memory_order_relaxed)), static_cast(computePipelinesCreatedLocked.load(std::memory_order_relaxed))); reportRecipes(); reportDispatchMove(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 5321104605..111098748f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -306,6 +306,7 @@ public: } // Whether RefreshData would record anything for `shader` (the per-word compare; verification). bool DataWordsDiffer(const CompiledShader& shader) const; + std::size_t BindingCount() const { return bindings.size(); } // Why the fast proof of a Revalidate left the object to the full walk (the [rescache] // revalidate line's reasons); Count: it did not. enum class FastFail : std::size_t { NoRecord, Collect, Pending, Evicted, Changed, Keys, ClearedView, StorageKeys, Depth, Replaced, Audit, Count }; From a84d3c46c8d9006c5ead37c63984e1bfc8cbea3d Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:02:48 -0300 Subject: [PATCH 064/428] feat(net): expose stream and datagram protocol --- core/libs/prx/libSceNet/CMakeLists.txt | 10 +++ core/libs/prx/libSceNet/Export.cpp | 12 ++++ core/libs/tests/GuestNetProtocol.cpp | 84 ++++++++++++++++++++++++++ docs/dev/debt/libSceNet.md | 2 + 4 files changed, 108 insertions(+) create mode 100644 core/libs/tests/GuestNetProtocol.cpp diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index 95faf4584b..435d5bb979 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -4,6 +4,16 @@ target_link_libraries(libSceNet PRIVATE libSceNetCtl) configure_windows_unwind(libSceNet) if(BUILD_TESTING) + add_test_executable(guest_net_protocol_tests ${LIBS_INCLUDE_DIR}/tests/GuestNetProtocol.cpp) + target_include_directories(guest_net_protocol_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_net_protocol_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(guest_net_protocol_tests) + add_test(NAME guest_net_protocol COMMAND guest_net_protocol_tests) + set_tests_properties(guest_net_protocol PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_net_protocol PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(net_dontwait_tests ${LIBS_INCLUDE_DIR}/tests/NetDontWait.cpp) target_include_directories(net_dontwait_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(net_dontwait_tests PRIVATE libSceNet libkernel libc) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 808cdc7364..8d1778fe51 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -273,6 +273,7 @@ constexpr int NET_TF_NODELAY = 4; constexpr int NET_SO_SNDTIMEO = 0x1005; constexpr int NET_SO_RCVTIMEO = 0x1006; constexpr int NET_SO_ERROR = 0x1007; +constexpr int NET_SO_PROTOCOL = 0x1016; constexpr int NET_SO_NBIO = 0x1200; constexpr int NET_SO_REUSEADDR = 0x4; constexpr int NET_SO_BROADCAST = 0x20; @@ -390,6 +391,7 @@ bool native_to_guest_address(const sockaddr_storage& native, void* address, std: struct Sock { int family = 0; int type = 0; + int protocol = 0; std::shared_ptr native; bool bound = false; bool listening = false; @@ -1006,6 +1008,9 @@ int APS5_VABI sceNetSocket(const char* name, int family, int type, int protocol) Sock s; s.family = family; s.type = type; + s.protocol = protocol; + if (protocol == 0 && type == NET_SOCK_STREAM) s.protocol = IPPROTO_TCP; + if (protocol == 0 && type == NET_SOCK_DGRAM) s.protocol = IPPROTO_UDP; s.native = std::move(handle); g_socks[fd] = s; return fd; @@ -1129,6 +1134,7 @@ int APS5_VABI sceNetAccept(int s, void* addr, uint32_t* addrlen) { Sock connection; connection.family = listener.family; connection.type = NET_SOCK_STREAM; + connection.protocol = listener.protocol; connection.native = std::move(handle); #ifdef _WIN32 connection.native->receiveTimeout = listener.native->receiveTimeout.load(); @@ -1536,6 +1542,12 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint socket = it->second; } if (optval == nullptr || optlen == nullptr) return fail(NET_EINVAL); + if (level == NET_SOL_SOCKET && optname == NET_SO_PROTOCOL) { + if (socket.type == NET_SOCK_RAW) NotImplemented_nid_no_patch(__func__); + *optlen = std::min(*optlen, static_cast(sizeof(socket.protocol))); + std::memcpy(optval, &socket.protocol, *optlen); + return 0; + } if (level == NET_SOL_SOCKET && (optname == NET_SO_RCVTIMEO || optname == NET_SO_SNDTIMEO)) { const int microseconds = optname == NET_SO_RCVTIMEO ? socket.native->receiveTimeout.load() : socket.native->sendTimeout.load(); const KernelTimeval timeout{microseconds / 1000000, microseconds % 1000000}; diff --git a/core/libs/tests/GuestNetProtocol.cpp b/core/libs/tests/GuestNetProtocol.cpp new file mode 100644 index 0000000000..01f247b3e0 --- /dev/null +++ b/core/libs/tests/GuestNetProtocol.cpp @@ -0,0 +1,84 @@ +#include +#include +#include +#include +#include +#include +#include +#include "prx/libc/include/general/VabiMacros.hpp" + +extern "C" { +int APS5_VABI sceNetInit_nid_postfix(); +int APS5_VABI sceNetSocket(const char*, int, int, int); +int APS5_VABI sceNetSocketClose(int); +int APS5_VABI sceNetGetsockopt(int, int, int, void*, std::uint32_t*); +int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); +int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI sceNetGetsockname(int, void*, std::uint32_t*); +int APS5_VABI sceNetListen(int, int); +int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); +int APS5_VABI sceNetAccept(int, void*, std::uint32_t*); +int* APS5_VABI sceNetErrnoLoc(); +} + +static void Require(bool condition, std::source_location where = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "check failed at line %u\n", where.line()); + std::abort(); + } +} + +static void CheckProtocol(int socket, int expected) { + for (const std::uint32_t capacity : {0u, 1u, 3u, 4u, 8u}) { + std::array bytes; + bytes.fill(0xa5); + auto size = capacity; + Require(sceNetGetsockopt(socket, 0xffff, 0x1016, bytes.data(), &size) == 0); + Require(size == std::min(capacity, 4u)); + Require(std::memcmp(bytes.data(), &expected, size) == 0); + Require(std::all_of(bytes.begin() + size, bytes.end(), [](auto value) { return value == 0xa5; })); + } +} + +int main() { + Require(sceNetInit_nid_postfix() == 0); + for (const int family : {2, 28}) { + for (const int type : {1, 2}) { + const int expected = type == 1 ? 6 : 17; + for (const int protocol : {0, expected}) { + const int socket = sceNetSocket("protocol", family, type, protocol); + Require(socket >= 0); + CheckProtocol(socket, expected); + std::uint32_t size = 4; + int value = 99; + Require(sceNetGetsockopt(socket, 0xffff, 0x1016, nullptr, &size) < 0 && *sceNetErrnoLoc() == 22); + Require(sceNetGetsockopt(socket, 0xffff, 0x1016, &value, nullptr) < 0 && *sceNetErrnoLoc() == 22); + Require(value == 99 && size == 4); + Require(sceNetSetsockopt(socket, 0xffff, 0x1016, &value, sizeof(value)) < 0); + CheckProtocol(socket, expected); + Require(sceNetSocketClose(socket) == 0); + Require(sceNetGetsockopt(socket, 0xffff, 0x1016, &value, &size) < 0 && *sceNetErrnoLoc() == 9); + Require(value == 99 && size == 4); + } + } + const int listener = sceNetSocket("protocol-listener", family, 1, 0); + const int client = sceNetSocket("protocol-client", family, 1, 6); + Require(listener >= 0 && client >= 0); + std::array address{}; + std::uint32_t size = family == 2 ? 16 : 28; + address[0] = size; + address[1] = family; + if (family == 2) { address[4] = 127; address[7] = 1; } + else address[23] = 1; + Require(sceNetBind_nid_postfix(listener, address.data(), size) == 0); + Require(sceNetListen(listener, 1) == 0); + CheckProtocol(listener, 6); + Require(sceNetGetsockname(listener, address.data(), &size) == 0); + Require(sceNetConnect(client, address.data(), size) == 0); + const int accepted = sceNetAccept(listener, nullptr, nullptr); + Require(accepted >= 0); + CheckProtocol(accepted, 6); + CheckProtocol(client, 6); + for (const int socket : {accepted, client, listener}) Require(sceNetSocketClose(socket) == 0); + } +} diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index f10d1eb873..e6430e89fe 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -7,6 +7,8 @@ ### Unknown function info +- [sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) - `SO_PROTOCOL` on raw sockets throws: FreeBSD's raw protocol-switch selection has not been mapped to host raw sockets. For stream and datagram sockets, the effective creation protocol and accepted-socket inheritance follow FreeBSD 11.4's [protocol tables](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/in_proto.c) and [sogetopt](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/kern/uipc_socket.c); the `0x1016` option and truncated integer output are not measured on PS5. Existing null-pointer validation still returns `EINVAL`. + - [sceNetConfigAddIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - disassembly confirms the same interface/address/broadcast/netmask arguments as SetIfaddr, entering its shared handler with add mode selected. GetIfaddr copies up to two 16-byte interface-address records. GetRouteInfo uses a family, record buffer, signed capacity and byte flags; internal statistics output is 16 bytes. The address-record and internal-statistics fields remain incomplete. - [Resolver-connect, thread creation, interface and diagnostic queries, WLAN configuration and DHCP queries](../../../core/libs/prx/libSceNet/Export.cpp) retain unsupported implementations. Their restored argument lists still reference incomplete connection, thread, interface, route, WLAN and statistics types. Kernel ioctl payloads need reconstruction from their handlers or consumers; buffer lengths alone do not establish field types. From 09efadc0fe05fe6f8286d13c2c18d36e054fbe55 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:05:51 -0300 Subject: [PATCH 065/428] fix(net): truncate TCP_NODELAY readback --- core/libs/prx/libSceNet/CMakeLists.txt | 11 +++ core/libs/prx/libSceNet/Export.cpp | 6 +- core/libs/tests/GuestNetNodelayReadback.cpp | 85 +++++++++++++++++++++ docs/dev/debt/libSceNet.md | 2 +- 4 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 core/libs/tests/GuestNetNodelayReadback.cpp diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index 95faf4584b..f6a693612d 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -77,4 +77,15 @@ if(BUILD_TESTING) set_tests_properties(guest_sce_net PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_net_nodelay_readback_tests ${LIBS_INCLUDE_DIR}/tests/GuestNetNodelayReadback.cpp) + target_include_directories(guest_net_nodelay_readback_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_net_nodelay_readback_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(guest_net_nodelay_readback_tests) + add_test(NAME guest_net_nodelay_readback COMMAND guest_net_nodelay_readback_tests) + set_tests_properties(guest_net_nodelay_readback PROPERTIES TIMEOUT 20) + if(WIN32) + target_link_libraries(guest_net_nodelay_readback_tests PRIVATE ws2_32) + set_tests_properties(guest_net_nodelay_readback PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 808cdc7364..8b39d4616f 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -1543,7 +1543,6 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint std::memcpy(optval, &timeout, *optlen); return 0; } - if (*optlen < sizeof(int)) return fail(NET_EINVAL); if (level == NET_IPPROTO_TCP && socket.type != NET_SOCK_STREAM) return fail(NET_EINVAL); if (level == NET_IPPROTO_TCP && optname == NET_TCP_NODELAY) { int enabled = 0; @@ -1551,10 +1550,11 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint if (::getsockopt(socket.native->value, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&enabled), &size) != 0) return fail(native_error()); const int value = enabled != 0 ? NET_TF_NODELAY : 0; - std::memcpy(optval, &value, sizeof(value)); - *optlen = sizeof(value); + *optlen = std::min(*optlen, static_cast(sizeof(value))); + std::memcpy(optval, &value, *optlen); return 0; } + if (*optlen < sizeof(int)) return fail(NET_EINVAL); if (level == NET_IPPROTO_TCP) return fail(NET_ENOPROTOOPT); if (level != NET_SOL_SOCKET) return fail(NET_EOPNOTSUPP); int value = 0; diff --git a/core/libs/tests/GuestNetNodelayReadback.cpp b/core/libs/tests/GuestNetNodelayReadback.cpp new file mode 100644 index 0000000000..c427cca3cb --- /dev/null +++ b/core/libs/tests/GuestNetNodelayReadback.cpp @@ -0,0 +1,85 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#include +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/Socket/include/SocketPoll.hpp" + +extern "C" { +int APS5_VABI sceNetInit_nid_postfix(); +int APS5_VABI sceNetSocket(const char*, int, int, int); +int APS5_VABI sceNetSocketClose(int); +int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); +int APS5_VABI sceNetGetsockopt(int, int, int, void*, std::uint32_t*); +int* APS5_VABI sceNetErrnoLoc(); +} + +static void Require(bool condition, std::source_location where = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "check failed at line %u\n", where.line()); + std::abort(); + } +} + +static void CheckValue(int socket, int expected) { + for (const std::uint32_t capacity : {0u, 1u, 2u, 3u, 4u, 8u}) { + std::array bytes; + bytes.fill(0xa5); + auto size = capacity; + Require(sceNetGetsockopt(socket, 6, 1, bytes.data(), &size) == 0); + Require(size == std::min(capacity, 4u)); + Require(std::memcmp(bytes.data(), &expected, size) == 0); + Require(std::all_of(bytes.begin() + size, bytes.end(), [](auto value) { return value == 0xa5; })); + } +} + +int main() { + Require(sceNetInit_nid_postfix() == 0); + for (const int family : {2, 28}) { + const int socket = sceNetSocket("nodelay", family, 1, 6); + const int other = sceNetSocket("nodelay-other", family, 1, 0); + const int datagram = sceNetSocket("nodelay-datagram", family, 2, 17); + Require(socket >= 0 && other >= 0 && datagram >= 0); + CheckValue(socket, 0); + for (const int requested : {1, 0, -1, 256, 0}) { + Require(sceNetSetsockopt(socket, 6, 1, &requested, sizeof(requested)) == 0); + CheckValue(socket, requested != 0 ? 4 : 0); + CheckValue(other, 0); + } + std::shared_ptr owner; + std::uintptr_t native = 0; + Require(KernelFindSocketNative_nid_no_patch(socket, &owner, &native)); + const int enabled = 1; + Require(::setsockopt(native, IPPROTO_TCP, TCP_NODELAY, + reinterpret_cast(&enabled), sizeof(enabled)) == 0); + CheckValue(socket, 4); + std::uint32_t size = 0; + int value = 99; + Require(sceNetGetsockopt(socket, 6, 1, nullptr, &size) < 0 && *sceNetErrnoLoc() == 22); + Require(sceNetGetsockopt(socket, 6, 1, &value, nullptr) < 0 && *sceNetErrnoLoc() == 22); + for (const std::uint32_t capacity : {0u, 3u, 4u}) { + size = capacity; + Require(sceNetGetsockopt(datagram, 6, 1, &value, &size) < 0 && *sceNetErrnoLoc() == 22); + Require(size == capacity && value == 99); + Require(sceNetGetsockopt(socket, 6, 0x7fffffff, &value, &size) < 0); + Require(*sceNetErrnoLoc() == (capacity < 4 ? 22 : 42)); + Require(size == capacity && value == 99); + } + owner.reset(); + for (const int descriptor : {socket, other, datagram}) Require(sceNetSocketClose(descriptor) == 0); + size = 0; + Require(sceNetGetsockopt(socket, 6, 1, &value, &size) < 0 && *sceNetErrnoLoc() == 9); + Require(size == 0 && value == 99); + } +} diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index f10d1eb873..fe45c0ca74 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -34,7 +34,7 @@ - [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) take `SO_SNDTIMEO` and `SO_RCVTIMEO` (0x1005, 0x1006) as a 16-byte `struct timeval`, as FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/uipc_socket.c#L2657-L2690) do, and Sony's 0x1105 and 0x1106 as an `int` of microseconds. A [console log of ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/issues/49) (firmware 12.20) shows receives timing out every 100 ms after it gave 0x1006 a `{0, 100000}` timeval. The errors for a value shorter than a timeval (`EINVAL`) or out of range (`EDOM`), and `sceNetGetsockopt` cutting the timeval to the caller's length, follow FreeBSD's `sooptcopyin` and `sooptcopyout` and are not measured on a console - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) stores `SO_ONESBCAST` (`0x10000`, [shadPS4's `ORBIS_NET_SO_ONESBCAST`](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net_types.h)) per socket and `sceNetGetsockopt` reads back the value that was set, as shadPS4 does; PPSA34501 sets it and crashes later when it fails with `EOPNOTSUPP`. Sends ignore it. FreeBSD 11.4's [ip(4)](https://man.freebsd.org/cgi/man.cgi?manpath=FreeBSD+11.4-RELEASE&query=ip&sektion=4) documents the like-named `IPPROTO_IP` option `IP_ONESBCAST`: a datagram sent to an interface's directed broadcast address, such as 192.168.2.255, leaves through that interface with its destination rewritten to 255.255.255.255, and only on a socket allowed to set `SO_BROADCAST`. Here host routing selects the outgoing interface and the destination is not rewritten. shadPS4 also leaves sends unchanged, and its [TODO](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp#L2467-L2470) reads the option the other way round: a send to 255.255.255.255 would be converted to the interface's broadcast address. Whether the console's `SOL_SOCKET` option behaves like `IP_ONESBCAST`, and what it reads back, is not verified on hardware. FreeBSD 11.4's `sogetopt()` reads several boolean socket options back as their flag bit (`so_options & optname`, `sys/kern/uipc_socket.c`), not as the value set; whether the console does so for `0x10000` is unknown - [sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reads `SO_ERROR` from the host socket. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `sceNetConnect`; a second failure with the same code on the same socket without a `sceNetConnect` in between would be hidden. -- [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) set `TCP_NODELAY` (`IPPROTO_TCP`, 1) on the host socket of a stream socket and read it back as `TF_NODELAY` (4) or 0, as FreeBSD 11.4's [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1626-L1650) does. Every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_NOPUSH` and `TCP_KEEPIDLE` among them, which FreeBSD implements) fails with `ENOPROTOOPT`, and the `IPPROTO_TCP` level on a socket that is not a stream socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/ip_output.c#L943-L944) answers it. A set on a connection that FreeBSD's [`tcp_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1498-L1502) refuses with `ECONNRESET` (refused, reset or in `TIME_WAIT`) succeeds here because the host accepts it. A null value with a nonzero length fails with `EINVAL`, where FreeBSD's `kern_setsockopt` returns `EFAULT`, and a get into less than an `int` fails with `EINVAL`, where `sooptcopyout` truncates. None of this was measured on a console. +- [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) - `TCP_MAXSEG`, `TCP_NOPUSH`, `TCP_KEEPIDLE` and other TCP options remain unsupported (`ENOPROTOOPT`). `TCP_NODELAY` sets on refused, reset or `TIME_WAIT` sockets can succeed through the host where FreeBSD 11.4's [tcp_ctloutput](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1498-L1502) returns `ECONNRESET`. A null setter value returns `EINVAL` rather than `kern_setsockopt`'s `EFAULT`. The non-stream `IPPROTO_TCP` error (`EINVAL`) and `TF_NODELAY` flag readback follow FreeBSD, not PS5 measurements. - [sceNetGetSockInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reports 0 for the name, pid, policy, priority, queue lengths, virtual ports, state, bandwidth, drops and waits, sets no status flag except `SELF` (0x1) and `NONBLOCK` (0x200000), and reports the host's buffer sizes where none was set. An IPv6 socket fails with `EAFNOSUPPORT` and is not counted or listed; shadPS4 lists it with zero addresses. `flags` bits other than 0x40 fail with `EINVAL`, where shadPS4 ignores `flags`; 0x40 adds sockets in TIME_WAIT ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs)), but closed sockets are not kept. Otherwise the `s` below 0 count and list, the return value and `EINVAL` for a null `info` or an `n` below 1 follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/06e813ff8c8e2a6d81b02cabfcc9543c3debf78e/src/core/libraries/net/net.cpp#L1498-L1522). Not measured on a console. - [sceNetSendmsg and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - console ordering between competing readers and writers is unmeasured. The per-call behavior follows FreeBSD 11.4's `soreceive_generic` and `sosend_generic` (`sys/kern/uipc_socket.c`); `0x80` is FreeBSD's flag value (`sys/sys/socket.h`). On Windows native sockets stay non-blocking. `MSG_DONTWAIT` and guest `SO_NBIO` return one host transfer's result. Blocking stream sends queue the whole buffer with one deadline and return progress after a timeout or host error. SSL uses this path through the native owner, retaining the socket, its mode and timeouts. Readiness waits poll at most every 10 ms for local close, so idle blocked operations wake periodically. From 6cf0937ccfd128f17612d348ba21a3d0113acf43 Mon Sep 17 00:00:00 2001 From: Kraken <10617703+UnhealthyKraken@users.noreply.github.com> Date: Thu, 22 Oct 2026 04:02:41 +0100 Subject: [PATCH 066/428] fix(libkernel): let variant timed joins use the canonical provider --- core/libs/prx/libkernel/CMakeLists.txt | 9 ++ .../prx/libkernel_sys/Export.Discovery.cpp | 5 -- .../tests/test_windows_timed_join_imports.py | 86 +++++++++++++++++++ 3 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 core/libs/tests/test_windows_timed_join_imports.py diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 641988178c..1c4c41c926 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1917,6 +1917,15 @@ if(BUILD_TESTING) target_include_directories(guest_timed_join_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_timed_join_tests PRIVATE libkernel libc ${CMAKE_DL_LIBS}) configure_windows_unwind(guest_timed_join_tests) + if(WIN32 AND Python3_Interpreter_FOUND) + foreach(joinProvider IN ITEMS libkernel_sys libkernel_web) + add_test(NAME guest_timed_join_import_${joinProvider} COMMAND ${Python3_EXECUTABLE} + ${LIBS_INCLUDE_DIR}/tests/test_windows_timed_join_imports.py $ ${PATCHED_DIR} + ${joinProvider}.prx $) + set_tests_properties(guest_timed_join_import_${joinProvider} PROPERTIES TIMEOUT 25 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endforeach() + endif() foreach(joinMode IN ITEMS normal coarse) add_test(NAME guest_timed_join_${joinMode} COMMAND guest_timed_join_tests $) set_tests_properties(guest_timed_join_${joinMode} PROPERTIES TIMEOUT 15) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 8b3da583e4..99b7da3626 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -523,11 +523,6 @@ int APS5_VABI pthread_switch_delete_np_nid_postfix(void) { return 0; } -int APS5_VABI pthread_timedjoin_np_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI ptrace_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/test_windows_timed_join_imports.py b/core/libs/tests/test_windows_timed_join_imports.py new file mode 100644 index 0000000000..4a5eeef09c --- /dev/null +++ b/core/libs/tests/test_windows_timed_join_imports.py @@ -0,0 +1,86 @@ +import os +import shutil +import struct +import subprocess +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / 'relinker' / 'relinker' / 'tests')) +from guest_import_fixtures import executable, fixture_directory + + +def call(code, slot): + code.extend(b'\xff\x15' + struct.pack(' Date: Thu, 22 Oct 2026 02:20:05 +0700 Subject: [PATCH 067/428] docs(libkernel): drop the stale 32-signal limit from the pthread_sigmask entry Co-Authored-By: Claude --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index b2cbdcc526..70e46c56b3 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -157,7 +157,7 @@ - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows suspends the target and redirects it to the handler. A target running host code (any module other than the executable and the `*.guest.prx` modules, so libkernel, libc and the other HLE modules count too) is resumed and sampled again until it is back in guest code, since a handler that waits there would keep the host locks that code holds; a target blocked in an ntdll wait is redirected in place, as are NtContinue and winpthreads after their retry limit. A target that stays in host code without waiting for 50 ms after the raise began, such as a long HLE or driver call, is then redirected in host code anyway, with the same hazard. A target seen right after a `syscall` instruction in ntdll, inside a system call that is not a wait such as the `NtWriteFile` of an `fwrite` holding its stream lock, restarts those 50 ms, so a raise can cost the raising thread up to the 1 s retry limit. While the target is suspended only `VirtualQuery` and a lock-free cache of image bases are consulted; an image not yet in the cache is classified with `GetModuleFileNameW` after the target is resumed. An APC queued at the first host-code sample, which an alertable guest wait would run sooner, is not implemented - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) builds the handler's `ucontext` with FreeBSD's amd64 layout (16-byte sigmask, `mcontext` at 0x40, `mc_rsp` at 0xf8); the console's layout is not verified beyond the fields PS5Util reads - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) does not consult the target thread's signal mask -- [pthread_sigmask](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) shares the process-wide mask of `sigprocmask` and `_sigprocmask`, which keeps only the first 32 signals; FreeBSD keeps a mask per thread +- [pthread_sigmask](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) shares the process-wide mask of `sigprocmask` and `_sigprocmask`; FreeBSD keeps a mask per thread - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) reaches a thread that waits in an alertable `TimedWait` wait through an APC. A thread blocked anywhere else, including all waits under `APS5_COARSE_TIMED_WAITS` and host waits inside the emulator, is redirected and runs the handler only when that wait returns - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs the handler wherever the target thread was interrupted. It waits only while the thread is inside winpthreads or restoring a context through `NtContinue`, and throws if that lasts 1 s, so a handler that calls into other host code the thread was interrupted in, such as the C runtime's heap or a lock of the emulator, can deadlock on it or reenter it. A thread that holds the address-wait lock of `sceKernelSyncOnAddressWait`, `sceKernelSyncOnAddressWake` and `_umtx_op`, or the park queue lock of the guest mutexes, gets the raise as an APC instead, which runs once it has released the lock or while it waits; the other locks of the emulator are not deferred this way - [sigaltstack](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records each thread's alternate signal stack and reports it back, following FreeBSD's `kern_sigaltstack` (`EINVAL` for flags other than `SS_DISABLE`, `ENOMEM` below `MINSIGSTKSZ` 2048), but handlers never run on it, so `SS_ONSTACK` and `EPERM` are never reported From 1d6c0bbb1f18599ec81e0aad9035c8121eae79ff Mon Sep 17 00:00:00 2001 From: thuanquang Date: Thu, 22 Oct 2026 02:20:26 +0700 Subject: [PATCH 068/428] docs(libSceNgs2.native): point the mastering voice gain entry at the AudioOut2 entries in libSceAudioOut Co-Authored-By: Claude --- docs/dev/debt/libSceNgs2.native.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index f5a4bd1856..8e740541bf 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -11,7 +11,7 @@ - [NGS2 submixer UserFx](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (control `0x20000004`) invokes the title's callback after input mixing on every playing grain, including silence. The 40-byte control and 48-byte process context follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/ngs2/ngs2_submixer.h). PPSA03747 installs a peak-meter callback; its code confirms the channel pointers, user data 0, channel count, grain size and sample-rate offsets and returns 0. Other user data words are preserved. Flags are assumed to be 1 after registration/restart and 0 thereafter, following UserFx2's first-process convention; this callback does not inspect them. Null handlers disable the effect, setup clears it, and nonzero callback results throw. These lifecycle and error semantics are unverified on console hardware. - [NGS2 voice callback after setup](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a sampler or mixer setup clears the voice callback with the rest of the voice state. PPSA18216 registers a block-end callback for a streamed music voice, later sets the same voice up again for one-shot ATRAC9 blocks with user data 0, and registers the callback again before it streams again; its callback dereferences the block's user data, so it must not run for the one-shot blocks. A setup made from inside the voice's own callback clears that callback as well, so a callback that sets its voice up again and queues the next block runs once instead of for every block; PPSA18216 never sets a voice up from inside its callback, so that ordering is unverified. Unverified on console hardware. - [NGS2 ATRAC9 streaming](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) keeps waveform duration and skip counts across compressed input blocks. The shared `APPEND` flag (2), called `ONLY_DATA` in the compressed-data reference, ignores an ATRAC9 continuation block's sample, skip and repeat counts, following [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/bb98f26433fa9e711c219aac1711fe8ecba5bf3a/src/libs/ngs2.cpp); PPSA03747 confirms a 262044-byte initial buffer declaring 11310532 samples, followed by 16384-byte buffers with flags 3. Incomplete superframes are retained until more input arrives. Input-block completion callbacks allow refills; the final callback of a logical waveform waits for its buffered PCM to drain, preserving the existing complete-block repeat behaviour. This buffering, callback timing, decoded-byte accounting and starvation silence are unverified on hardware. A closed ATRAC9 stream ending before its declared samples throws; fragmented ATRAC9 waveforms with repeats are unsupported. PCM streaming retains its existing append validation and early-close behaviour. -- [NGS2 mastering voice gain](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - the param id 0x30000004 is taken from PPSA22520 and the layout from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2MasteringVoiceGainParam`; no reference gives the units or range. The levels are assumed to be linear factors applied when the mastering voice is mixed into the render buffer, so the voice's own samples stay unscaled; the LFE level applies to channel 3 of 6- and 8-channel voices, assuming the L R C LFE order of the AudioOut2 entries above, and the full-band level to the rest. Negative levels are accepted, and a new setup resets both levels to 1 +- [NGS2 mastering voice gain](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - the param id 0x30000004 is taken from PPSA22520 and the layout from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2MasteringVoiceGainParam`; no reference gives the units or range. The levels are assumed to be linear factors applied when the mastering voice is mixed into the render buffer, so the voice's own samples stay unscaled; the LFE level applies to channel 3 of 6- and 8-channel voices, assuming the L R C LFE order of the `sceAudioOut2PortCreate` entries in [libSceAudioOut](libSceAudioOut.md), and the full-band level to the rest. Negative levels are accepted, and a new setup resets both levels to 1 - [NGS2 stereo mastering voice into a surround buffer](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - PPSA21402 renders a 2-channel mastering voice into a 6- or 8-channel render buffer, which the mixing stage rejected. The console's conversion is unknown: the left and right channels are assumed to go to the first two buffer channels (front left and right) and the remaining channels stay silent, with no centre, LFE or surround content. Any other channel count mismatch still throws - [NGS2 voices without a format](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a sampler voice whose setup has no format (all zero) accepts an add of no waveform blocks without `APPEND`, which only applies `RESET` and the `CONTINUE` state, and matrix levels with no levels, which clear that matrix. PPSA36591 sets such a voice up, adds no blocks and sets no levels when a button is pressed at its title screen. Whether the console returns success or an error there is not measured; blocks with data, `APPEND` and an empty matrix on a voice with a format are still refused - [NGS2 silent waveform blocks](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native): flag `0x10` (named `SILENCE` here; no public reference names it) lets a waveform block declare samples without data. PPSA04867 adds one block with flags `0x11`, `data_size` 0 and `num_samples` 16385, then another with flags `0x11` and a fully specified 0x800-byte, 1024-sample mono block, which takes the ordinary path. A dataless block under the flag outputs `num_samples` frames of silence, reports them through `decoded_data_size`, and finishes like a complete block; a dataless block without the flag still throws as not fitting its data. The flag's real name, its interaction with ATRAC9 and repeats, and the console's `waveform_data` for silent blocks are unverified on hardware From ca528756525dc9a53370c08a90f6ccb4d8bf7211 Mon Sep 17 00:00:00 2001 From: dantdmdarealguy Date: Wed, 21 Oct 2026 17:38:34 -0500 Subject: [PATCH 069/428] build(windows): guard NOMINMAX in the long host path fopen test --- core/libs/tests/GuestFopenLongHostPath.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestFopenLongHostPath.cpp b/core/libs/tests/GuestFopenLongHostPath.cpp index 9ebe1738ce..3b31a2c4b0 100644 --- a/core/libs/tests/GuestFopenLongHostPath.cpp +++ b/core/libs/tests/GuestFopenLongHostPath.cpp @@ -7,7 +7,9 @@ #include #include #include +#ifndef NOMINMAX #define NOMINMAX +#endif #include extern "C" { From 600efd95be487522f0a6069f0f139c1499bc118e Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Thu, 22 Oct 2026 01:13:11 -0300 Subject: [PATCH 070/428] fix(relinker): leave an already executable autorun output's mode alone Autorun added the execute bits with an unconditional chmod, even when the output already had them. On macOS a chmod changes the file's ctime, and Rosetta then throws away its translation of an x86_64 executable and translates it again on the next exec, about 0.11 s per run on an M2 Max. autorun_exit_status paid that on every POSIX run: it chmods the child and also renames it once per quoting case, and a rename changes the ctime too. On the x86_64 macOS relinker job it takes 5 s at the median and timed out once at its 20 s limit, against 0.2 s on arm64. Autorun now only calls chmod when an execute bit is missing, and the test runs symlinks to itself instead of copying and renaming the executable. It also checks that the executable's ctime does not change and that Autorun still makes a non-executable output executable. --- core/relinker/cli/src/Autorun.cpp | 9 ++--- core/relinker/cli/tests/AutorunExitTests.cpp | 41 ++++++++++++++++---- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/core/relinker/cli/src/Autorun.cpp b/core/relinker/cli/src/Autorun.cpp index aa54150b47..e6e6defd69 100644 --- a/core/relinker/cli/src/Autorun.cpp +++ b/core/relinker/cli/src/Autorun.cpp @@ -16,11 +16,10 @@ namespace Cli { int Autorun(const std::string& absPath, bool toWindows) { if (!toWindows) { - std::filesystem::permissions(Io::NativePath(absPath), - std::filesystem::perms::owner_exec | - std::filesystem::perms::group_exec | - std::filesystem::perms::others_exec, - std::filesystem::perm_options::add); + const auto path = Io::NativePath(absPath); + constexpr auto executable = std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | std::filesystem::perms::others_exec; + if ((std::filesystem::status(path).permissions() & executable) != executable) + std::filesystem::permissions(path, executable, std::filesystem::perm_options::add); } int rawCode = 0; diff --git a/core/relinker/cli/tests/AutorunExitTests.cpp b/core/relinker/cli/tests/AutorunExitTests.cpp index c4883221a5..0e833ca7ad 100644 --- a/core/relinker/cli/tests/AutorunExitTests.cpp +++ b/core/relinker/cli/tests/AutorunExitTests.cpp @@ -3,7 +3,11 @@ #include #include #include +#ifndef _WIN32 +#include +#endif #include +#include #include #include #include @@ -21,26 +25,43 @@ int main(int, char* argv[]) { std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + " with spaces" + executable.extension().string()); #ifndef _WIN32 const auto signaled = child.parent_path() / ("anyps5-autorun-child-signal-" + child.filename().string()); + const auto script = child.parent_path() / ("anyps5-autorun-script-" + child.filename().string()); + const auto changeTime = [](const std::filesystem::path& path) { + struct stat info {}; + if (stat(path.c_str(), &info) != 0) throw std::runtime_error("Cannot stat " + path.string()); +#ifdef __APPLE__ + return info.st_ctimespec.tv_sec * 1000000000LL + info.st_ctimespec.tv_nsec; +#else + return info.st_ctim.tv_sec * 1000000000LL + info.st_ctim.tv_nsec; +#endif + }; #endif std::istringstream input("\n\n\n"); auto* savedInput = std::cin.rdbuf(input.rdbuf()); int result = 0; + std::vector aliases; try { +#ifdef _WIN32 std::filesystem::copy_file(executable, child); +#else + std::filesystem::create_symlink(executable, child); + const auto changed = changeTime(executable); +#endif std::vector suffixes{""}; #ifndef _WIN32 suffixes.insert(suffixes.end(), {" $HOME", " $(printf substituted)", " `printf substituted`", " 'single'", " \"double\"", " \\backslash", " line\nbreak"}); #endif - const auto originalChild = child; for (const auto& suffix : suffixes) { - const auto renamed = std::filesystem::path(originalChild.string() + suffix); - if (renamed != child) std::filesystem::rename(child, renamed); - child = renamed; + const auto alias = std::filesystem::path(child.string() + suffix); + if (alias != child) { + std::filesystem::create_symlink(child, alias); + aliases.push_back(alias); + } for (const bool windows : {false, true}) { - const int code = Cli::Autorun(child.string(), windows); + const int code = Cli::Autorun(alias.string(), windows); if (code != 42) throw std::runtime_error("Autorun changed exit code 42 to " + - std::to_string(code) + " for " + child.string()); + std::to_string(code) + " for " + alias.string()); } } #ifndef _WIN32 @@ -66,18 +87,24 @@ int main(int, char* argv[]) { } if (std::signal(SIGCHLD, savedHandler) == SIG_ERR) throw std::runtime_error("Cannot restore SIGCHLD after the autorun failure test"); - std::filesystem::rename(child, signaled); + std::filesystem::create_symlink(child, signaled); const int code = Cli::Autorun(signaled.string(), false); if (code != 128 + SIGILL) throw std::runtime_error("Autorun lost the child signal"); + if (changeTime(executable) != changed) throw std::runtime_error("Autorun changed the mode of an output that was already executable"); + std::ofstream(script) << "#!/bin/sh\nexit 42\n"; + std::filesystem::permissions(script, std::filesystem::perms::owner_read | std::filesystem::perms::owner_write); + if (Cli::Autorun(script.string(), false) != 42) throw std::runtime_error("Autorun did not make a non-executable output executable"); #endif } catch (const std::exception& error) { std::cerr << error.what() << '\n'; result = 1; } std::cin.rdbuf(savedInput); + for (const auto& alias : aliases) std::filesystem::remove(alias); std::filesystem::remove(child); #ifndef _WIN32 std::filesystem::remove(signaled); + std::filesystem::remove(script); #endif return result; } From 00d6b33126904043ced47dcd98a99282c50fe164 Mon Sep 17 00:00:00 2001 From: Adnan Khan Date: Thu, 22 Oct 2026 12:48:19 +0530 Subject: [PATCH 071/428] feat(relinker): report needed modules shipped outside the module directories --- core/relinker/cli/include/NativeModules.hpp | 4 + core/relinker/cli/src/NativeModules.cpp | 84 +++++++++++++++---- core/relinker/main.cpp | 8 +- .../tests/test_guest_needed_modules.py | 60 +++++++++++++ docs/user/USAGE.md | 2 + 5 files changed, 141 insertions(+), 17 deletions(-) diff --git a/core/relinker/cli/include/NativeModules.hpp b/core/relinker/cli/include/NativeModules.hpp index 66139266ca..31d54f5477 100644 --- a/core/relinker/cli/include/NativeModules.hpp +++ b/core/relinker/cli/include/NativeModules.hpp @@ -1,11 +1,13 @@ #pragma once #include +#include namespace Cli { struct NativeModules { std::filesystem::path Root; + std::vector Directories; std::vector RuntimeDirectories; std::vector Modules; }; @@ -14,4 +16,6 @@ bool SameDirectory(const std::filesystem::path& first, const std::filesystem::pa NativeModules ReadModules(const std::filesystem::path& inputPath, const std::filesystem::path& sceModulePath, const std::set& excludedModules, const std::vector& moduleDirectories); +void ReportUnconvertedNeeded(const NativeModules& modules, const std::vector& neededLibraries, std::ostream& out); + } diff --git a/core/relinker/cli/src/NativeModules.cpp b/core/relinker/cli/src/NativeModules.cpp index f01ac374d4..a55ea22605 100644 --- a/core/relinker/cli/src/NativeModules.cpp +++ b/core/relinker/cli/src/NativeModules.cpp @@ -2,13 +2,46 @@ #include #include #include +#include #include +#include #include #include #include +#include +#include namespace Cli { +namespace { + +struct CandidateHeader { + bool Elf = false; + bool Self = false; + std::uint16_t Type = 0; +}; + +CandidateHeader ReadCandidateHeader(const std::filesystem::path& path) { + std::ifstream stream(path, std::ios::binary); + if (!stream) throw Domain::RelinkerException("Cannot read guest candidate: " + Io::Utf8Path(path)); + char header[18]{}; + stream.read(header, sizeof header); + if (stream.bad()) throw Domain::RelinkerException("Cannot read guest candidate magic: " + Io::Utf8Path(path)); + CandidateHeader result; + if (stream.gcount() < 4) return result; + const auto byte = [&](const std::size_t index) { return static_cast(header[index]); }; + result.Self = (byte(0) == 0x4f && byte(1) == 0x15 && byte(2) == 0x3d && byte(3) == 0x1d) || (byte(0) == 0x54 && byte(1) == 0x14 && byte(2) == 0xf5 && byte(3) == 0xee); + result.Elf = byte(0) == 0x7f && header[1] == 'E' && header[2] == 'L' && header[3] == 'F'; + if (result.Elf && stream.gcount() == sizeof header) result.Type = static_cast(byte(16) | (byte(17) << 8)); + return result; +} + +std::string_view ModuleStem(const std::string_view name) { + return name.substr(0, name.size() - Domain::ModuleSuffix(name).size()); +} + +} + bool SameDirectory(const std::filesystem::path& first, const std::filesystem::path& second) { if (std::filesystem::exists(first) && std::filesystem::exists(second)) return std::filesystem::equivalent(first, second); return std::filesystem::weakly_canonical(first) == std::filesystem::weakly_canonical(second); @@ -45,17 +78,6 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil } std::vector paths; std::set unmatchedExclusions = excludedModules; - const auto isElf = [](const std::filesystem::path& path, bool& self) { - std::ifstream stream(path, std::ios::binary); - if (!stream) throw Domain::RelinkerException("Cannot read guest candidate: " + Io::Utf8Path(path)); - char magic[4]{}; - stream.read(magic, 4); - if (stream.bad()) throw Domain::RelinkerException("Cannot read guest candidate magic: " + Io::Utf8Path(path)); - if (stream.gcount() != 4) return false; - const auto byte = [&](const std::size_t index) { return static_cast(magic[index]); }; - self = (byte(0) == 0x4f && byte(1) == 0x15 && byte(2) == 0x3d && byte(3) == 0x1d) || (byte(0) == 0x54 && byte(1) == 0x14 && byte(2) == 0xf5 && byte(3) == 0xee); - return byte(0) == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F'; - }; for (const auto& directory : directories) { if (!std::filesystem::is_directory(directory)) throw Domain::RelinkerException("Guest module path is not a directory: " + Io::Utf8Path(directory)); for (const auto& entry : std::filesystem::directory_iterator(directory)) { @@ -66,9 +88,9 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil } if (!entry.is_regular_file()) continue; if (Io::SameFile(entry.path(), inputPath)) continue; - bool self = false; - if (isElf(entry.path(), self)) paths.push_back(entry.path()); - else if (self) + const auto header = ReadCandidateHeader(entry.path()); + if (header.Elf) paths.push_back(entry.path()); + else if (header.Self) std::cout << "Skipping SELF container: " << entry.path().lexically_relative(root).generic_string() << " (unwrap it before relinking)\n"; } @@ -77,6 +99,7 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil std::sort(paths.begin(), paths.end()); NativeModules result; result.Root = root; + result.Directories = std::move(directories); result.RuntimeDirectories = std::move(runtimeDirectories); for (const auto& path : paths) result.Modules.push_back({path, Io::FileReader().Read(Io::Utf8Path(path))}); std::map sonames; @@ -107,4 +130,37 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil return result; } +void ReportUnconvertedNeeded(const NativeModules& modules, const std::vector& neededLibraries, std::ostream& out) { + std::map> stems; + for (const auto& name : neededLibraries) + if (!Domain::ModuleSuffix(name).empty()) stems[std::string(ModuleStem(name))].push_back(name); + if (stems.empty()) return; + struct Found { + std::filesystem::path Relative; + std::string Name; + bool Self; + }; + std::vector found; + for (auto entry = std::filesystem::recursive_directory_iterator(modules.Root, std::filesystem::directory_options::skip_permission_denied); entry != std::filesystem::recursive_directory_iterator(); ++entry) { + if (entry->is_directory()) { + if (std::any_of(modules.Directories.begin(), modules.Directories.end(), [&](const auto& directory) { return SameDirectory(directory, entry->path()); })) entry.disable_recursion_pending(); + continue; + } + if (!entry->is_regular_file()) continue; + const auto filename = Io::Utf8Path(entry->path().filename()); + if (Domain::ModuleSuffix(filename).empty()) continue; + const auto stem = stems.find(std::string(ModuleStem(filename))); + if (stem == stems.end()) continue; + const auto header = ReadCandidateHeader(entry->path()); + if (!header.Self && !(header.Elf && header.Type == 0xfe18)) continue; + for (const auto& name : stem->second) found.push_back({entry->path().lexically_relative(modules.Root), name, header.Self}); + } + std::sort(found.begin(), found.end(), [](const auto& left, const auto& right) { return std::tie(left.Relative, left.Name) < std::tie(right.Relative, right.Name); }); + for (const auto& [relative, name, self] : found) { + const auto directory = relative.parent_path().generic_string(); + out << "Needed module not converted: " << name << " at " << relative.generic_string() << "; add --module-dir " << (directory.empty() ? "." : directory) + << (self ? " and unwrap the SELF container" : "") << '\n'; + } +} + } diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 98bca27396..49ae7b38ac 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -45,11 +45,12 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, input.Executable = fileReader.Read(inputPath); input.InputPath = std::filesystem::absolute(Io::NativePath(inputPath)).lexically_normal(); input.OutputPath = Io::NativePath(absPath); + Cli::NativeModules modules; if (!args.skipSceModule) { - auto modules = Cli::ReadModules(std::filesystem::absolute(Io::NativePath(args.inputPath)).lexically_normal(), Io::NativePath(args.sceModulePath), args.excludedSceModules, args.moduleDirectories); - input.ModuleRoot = std::move(modules.Root); + modules = Cli::ReadModules(std::filesystem::absolute(Io::NativePath(args.inputPath)).lexically_normal(), Io::NativePath(args.sceModulePath), args.excludedSceModules, args.moduleDirectories); + input.ModuleRoot = modules.Root; input.Modules = std::move(modules.Modules); - input.RuntimeDirectories = std::move(modules.RuntimeDirectories); + input.RuntimeDirectories = modules.RuntimeDirectories; } const auto iconPath = Io::NativePath(args.inputPath).parent_path() / "sce_sys" / "icon0.png"; if (args.toWindows && std::filesystem::exists(iconPath)) input.Icon = fileReader.Read(Io::Utf8Path(iconPath)); @@ -75,6 +76,7 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, progress.IncludeFile(Io::Utf8Path(input.OutputPath)); for (const auto& module : input.Modules) progress.IncludeFile(Io::Utf8Path(module.Path)); auto converted = Relinker::Conversion(progress).Convert(std::move(input), options); + if (!args.skipSceModule) Cli::ReportUnconvertedNeeded(modules, converted.Analysis.DynamicSection.NeededLibraries, std::cout); return {inputPath, absPath, std::move(converted.Analysis), std::move(converted.Modules), std::move(converted.Executable), std::move(converted.WindowsDiagnostics), std::move(converted.AppliedPatches), converted.LinuxBase}; } diff --git a/core/relinker/relinker/tests/test_guest_needed_modules.py b/core/relinker/relinker/tests/test_guest_needed_modules.py index 6b2b18f2c8..99295fd68c 100644 --- a/core/relinker/relinker/tests/test_guest_needed_modules.py +++ b/core/relinker/relinker/tests/test_guest_needed_modules.py @@ -86,11 +86,71 @@ def check_guest_needed(relinker, work): assert not output.exists() and not (case / "app0").exists(), output +def check_needed_outside(relinker, work): + self_container = bytes.fromhex("4f153d1d") + b"\0" * 60 + sce_module = module_with_symbol(True) + struct.pack_into(" An explicit `--module-dir` also works when the module parent has no `sce_module`, `sce_modules` or `prx` directory. For a homebrew bundle with shared libraries beside its executable, use `--module-dir .`; no empty standard directory is required. Without an explicit module directory, the standard-directory requirement remains. +A module the executable needs that is not converted is looked for under the module parent directory, outside the module directories. Each SELF container or `ET_SCE_DYNAMIC` ELF found there whose name matches, up to the `.prx`, `.sprx`, `.suprx` or `.debug_prx` suffix, is reported after the conversion with the `--module-dir` that would include it; the output still refers to a host library of that name. + Linux output: ```sh From 02eee49df45b094ba9d1a7d14dec8acce23fd236 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Thu, 22 Oct 2026 09:27:23 +0200 Subject: [PATCH 072/428] docs(debt): make the NpSns entry and its note agree on three names The first entry in the libSceNpSns debt file lists 24 names, but Export.cpp declares 21. The note two entries below says that sceNpSnsFacebookCreateRequest, sceNpSnsFacebookDeleteRequest and sceNpSnsFacebookGetAccessToken are omitted from this module because another module declares them. They are declared in libSceNpManager (Export.cpp, since f04d16d702), not in libSceNpSnsFacebookDialog by #6977 as the note still says. The three names are removed from the entry so that it names exactly the 21 declared functions, and the note now names libSceNpManager. No code changes. Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceNpSns.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceNpSns.md b/docs/dev/debt/libSceNpSns.md index 9f14bbcc3a..29b552135c 100644 --- a/docs/dev/debt/libSceNpSns.md +++ b/docs/dev/debt/libSceNpSns.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceNpSnsFacebookAbortRequest, sceNpSnsFacebookCreateRequest, sceNpSnsFacebookDeleteRequest, sceNpSnsFacebookGetAccessToken, sceNpSnsIntAbortRequest, sceNpSnsIntCreateRequest, sceNpSnsIntDeleteRequest, sceNpSnsIntFbGetGameAccessToken, sceNpSnsIntFbGetGameAccessTokenAllowed, sceNpSnsIntFbGetSystemAccessToken, sceNpSnsIntFbGetTitleInfo, sceNpSnsIntLinkedStatus, sceNpSnsIntTwGetSystemAccessToken, sceNpSnsIntUnlink, sceNpSnsIntYtGetAccessToken, sceNpSnsIntYtRefreshMasterToken, sceNpSnsTwitchAbortRequest, sceNpSnsTwitchCreateRequest, sceNpSnsTwitchDeleteRequest, sceNpSnsTwitchGetAccessToken, sceNpSnsYouTubeAbortRequest, sceNpSnsYouTubeCreateRequest, sceNpSnsYouTubeDeleteRequest and sceNpSnsYouTubeGetAccessToken](../../../core/libs/prx/libSceNpSns/Export.cpp) (libSceNpSns) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpSns.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceNpSnsFacebookAbortRequest, sceNpSnsIntAbortRequest, sceNpSnsIntCreateRequest, sceNpSnsIntDeleteRequest, sceNpSnsIntFbGetGameAccessToken, sceNpSnsIntFbGetGameAccessTokenAllowed, sceNpSnsIntFbGetSystemAccessToken, sceNpSnsIntFbGetTitleInfo, sceNpSnsIntLinkedStatus, sceNpSnsIntTwGetSystemAccessToken, sceNpSnsIntUnlink, sceNpSnsIntYtGetAccessToken, sceNpSnsIntYtRefreshMasterToken, sceNpSnsTwitchAbortRequest, sceNpSnsTwitchCreateRequest, sceNpSnsTwitchDeleteRequest, sceNpSnsTwitchGetAccessToken, sceNpSnsYouTubeAbortRequest, sceNpSnsYouTubeCreateRequest, sceNpSnsYouTubeDeleteRequest and sceNpSnsYouTubeGetAccessToken](../../../core/libs/prx/libSceNpSns/Export.cpp) (libSceNpSns) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpSns.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified ### Inventory exports declared elsewhere - sceNpSnsFacebookCreateRequest, sceNpSnsFacebookDeleteRequest and sceNpSnsFacebookGetAccessToken are also 3.20 exports of this module but are currently declared by #6977 in libSceNpSnsFacebookDialog, so they are omitted here to respect the duplicate-export rule; re-add if that PR's module attribution is rejected. + sceNpSnsFacebookCreateRequest, sceNpSnsFacebookDeleteRequest and sceNpSnsFacebookGetAccessToken are also 3.20 exports of this module but are declared in `libSceNpManager` ([`Export.cpp`](../../../core/libs/prx/libSceNpManager/Export.cpp)), so they are omitted here to respect the duplicate-export rule; re-add them here if that module attribution is corrected. From 89031ca7b95be67ac5213e55ce5502b8a5b69ef8 Mon Sep 17 00:00:00 2001 From: Itamar Medyoni Date: Wed, 21 Oct 2026 23:54:27 +0300 Subject: [PATCH 073/428] fix(libkernel): reject clocks libthr does not accept in pthread_condattr_setclock FreeBSD 11 libthr's pthread_condattr_setclock returns EINVAL for any clock other than CLOCK_REALTIME, CLOCK_VIRTUAL, CLOCK_PROF and CLOCK_MONOTONIC. scePthreadCondattrSetclock stored any clock id; it now answers SCE_KERNEL_ERROR_EINVAL for the others, and pthread_condattr_setclock EINVAL through it. --- core/libs/prx/libkernel/Pthread/src/Cond.cpp | 2 ++ core/libs/tests/GuestPthreadCondattr.cpp | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Pthread/src/Cond.cpp b/core/libs/prx/libkernel/Pthread/src/Cond.cpp index a3a3087bf7..34b88bfd80 100644 --- a/core/libs/prx/libkernel/Pthread/src/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Cond.cpp @@ -153,7 +153,9 @@ int APS5_VABI scePthreadCondattrDestroy(PthreadCondattr* attr) { } int APS5_VABI scePthreadCondattrSetclock(PthreadCondattr* attr, KernelClockid clockId) { + constexpr KernelClockid realtime = 0, virtualTime = 1, profiling = 2, monotonic = 4; if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + if (clockId != realtime && clockId != virtualTime && clockId != profiling && clockId != monotonic) return SCE_KERNEL_ERROR_EINVAL; (*attr)->_clockid = static_cast(clockId); return 0; } diff --git a/core/libs/tests/GuestPthreadCondattr.cpp b/core/libs/tests/GuestPthreadCondattr.cpp index 237ac4374e..4974beb04a 100644 --- a/core/libs/tests/GuestPthreadCondattr.cpp +++ b/core/libs/tests/GuestPthreadCondattr.cpp @@ -34,11 +34,15 @@ int main() { PthreadCondattr attr = nullptr; Require(scePthreadCondattrInit(&attr) == SCE_OK); - for (const KernelClockid clock : {0, 1, 2, Monotonic, 5, 12}) { + for (const KernelClockid clock : {0, 1, 2, Monotonic}) { Require(scePthreadCondattrSetclock(&attr, clock) == SCE_OK); Require(pthread_condattr_setclock_nid_postfix(&attr, clock) == 0); } Require(pthread_condattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 0); + for (const KernelClockid clock : {-1, 3, 5, 12, 13, 14, 15}) { + Require(scePthreadCondattrSetclock(&attr, clock) == SceEinval); + Require(pthread_condattr_setclock_nid_postfix(&attr, clock) == GuestEinval); + } for (const int invalid : {1, 2, -1}) { Require(pthread_condattr_setpshared_nid_postfix(&attr, invalid) == GuestEinval); sharing = -1; From 36068243141fa08d231c0a9a103e54a0a505968f Mon Sep 17 00:00:00 2001 From: frankea Date: Wed, 21 Oct 2026 22:30:10 -0400 Subject: [PATCH 074/428] fix(shader): find strongly connected components without recursion computeStronglyConnectedComponents visited blocks recursively, one stack frame per block on the deepest path. The reach-flag fallback turns Ghostrunner's compute shader 0x200c520000 into a graph of 32,490 blocks, and with APS5_FORCE_STRUCTURIZER_FALLBACK the visit overflowed agc_shader_replay's 2 MiB stack after 4 s. The visit now keeps its frames in a vector. It enters successors in the same order and closes components in the same order, so the components, their entry blocks and the irreducibility verdict are unchanged. --- .../ControlFlow/src/Structurizer.cpp | 61 ++++++++++++------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index 3267461138..42bd489700 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -1575,27 +1575,7 @@ struct TarjanState { std::vector components; }; -void tarjanVisit(TarjanState& state, std::uint32_t blockId) { - state.index[blockId] = state.nextIndex; - state.lowlink[blockId] = state.nextIndex; - ++state.nextIndex; - state.stack.push_back(blockId); - state.onStack[blockId] = true; - - const auto& block = state.graph->FindBlock(blockId); - for (const auto successor : block.successors) { - if (state.index[successor] == InvalidControlFlowId) { - tarjanVisit(state, successor); - state.lowlink[blockId] = std::min(state.lowlink[blockId], state.lowlink[successor]); - } else if (state.onStack[successor]) { - state.lowlink[blockId] = std::min(state.lowlink[blockId], state.index[successor]); - } - } - - if (state.lowlink[blockId] != state.index[blockId]) { - return; - } - +void emitComponent(TarjanState& state, std::uint32_t blockId) { StronglyConnectedComponent component; for (;;) { const auto member = state.stack.back(); @@ -1625,6 +1605,45 @@ void tarjanVisit(TarjanState& state, std::uint32_t blockId) { state.components.push_back(std::move(component)); } +void tarjanVisit(TarjanState& state, std::uint32_t root) { + struct Frame { + std::uint32_t blockId; + const std::vector* successors; + std::size_t next; + }; + std::vector work; + const auto enter = [&](std::uint32_t blockId) { + state.index[blockId] = state.nextIndex; + state.lowlink[blockId] = state.nextIndex; + ++state.nextIndex; + state.stack.push_back(blockId); + state.onStack[blockId] = true; + work.push_back({blockId, &state.graph->FindBlock(blockId).successors, 0u}); + }; + enter(root); + while (!work.empty()) { + auto& frame = work.back(); + const auto blockId = frame.blockId; + if (frame.next < frame.successors->size()) { + const auto successor = (*frame.successors)[frame.next++]; + if (state.index[successor] == InvalidControlFlowId) { + enter(successor); + } else if (state.onStack[successor]) { + state.lowlink[blockId] = std::min(state.lowlink[blockId], state.index[successor]); + } + continue; + } + work.pop_back(); + if (!work.empty()) { + auto& parent = state.lowlink[work.back().blockId]; + parent = std::min(parent, state.lowlink[blockId]); + } + if (state.lowlink[blockId] == state.index[blockId]) { + emitComponent(state, blockId); + } + } +} + bool forcedFlagStructurization() { static const bool forced = std::getenv("APS5_FORCE_STRUCTURIZER_FALLBACK") != nullptr; return forced; From 0bfda652563627aecc152823bf4d1296b023faba Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Thu, 22 Oct 2026 10:28:32 +0200 Subject: [PATCH 075/428] docs(libSceAvPlayer): record the unmeasured order of the trick speed checks after the source check --- docs/dev/debt/libSceAvPlayer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAvPlayer.md b/docs/dev/debt/libSceAvPlayer.md index 2e7cd27eee..9f68138b00 100644 --- a/docs/dev/debt/libSceAvPlayer.md +++ b/docs/dev/debt/libSceAvPlayer.md @@ -3,7 +3,7 @@ ### Silent stubs - [libSceAvPlayer](../../../core/libs/prx/libSceAvPlayer/Export.cpp): `sceAvPlayerSetLogCallback` accepts a callback that is never called, as the player produces no log messages, and `sceAvPlayerSetAvailableBandwidth` has no effect, as it governs HLS sources, which `sceAvPlayerAddSource` does not implement; after a source is added it returns `OPERATION_FAILED`, as [prosper's reading of the module](https://github.com/mattias800/prosper/blob/8dcb5dfdb9997ede7c76783995010d6abd1222f8/prosper/src/hle/video/avplayer.cpp#L960-L972) describes. -- [sceAvPlayerSetTrickSpeed](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) with a negative speed runs the clock backwards but delivers no frames; when a forward speed is set again, playback resumes from the rewound time. +- [sceAvPlayerSetTrickSpeed](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) with a negative speed runs the clock backwards but delivers no frames; when a forward speed is set again, playback resumes from the rewound time. With a source, a zero speed returns `INVALID_PARAMS` before the `OPERATION_FAILED` of a stopped, finished or failed player, the order AnyPS5 had before; that order is unmeasured. In [prosper's reading of the module](https://github.com/mattias800/prosper/blob/8dcb5dfdb9997ede7c76783995010d6abd1222f8/prosper/src/hle/video/avplayer.cpp#L929-L959), only the missing source is checked first. A zero speed is refused with `NOT_SUPPORTED`, with every other speed from -399 to 399 except 100, and no player state is checked. ### Unknown function info From f3dcf6d8bef1cf4a27258aef2ffe11e75dec8056 Mon Sep 17 00:00:00 2001 From: Adnan Khan Date: Thu, 22 Oct 2026 14:02:37 +0530 Subject: [PATCH 076/428] fix(relinker): skip unreadable candidates and fold case for --windows in the needed-module report --- core/relinker/cli/include/NativeModules.hpp | 2 +- core/relinker/cli/src/NativeModules.cpp | 34 +++++++++++++------ core/relinker/main.cpp | 2 +- .../tests/test_guest_needed_modules.py | 18 ++++++++++ docs/user/USAGE.md | 2 +- 5 files changed, 45 insertions(+), 13 deletions(-) diff --git a/core/relinker/cli/include/NativeModules.hpp b/core/relinker/cli/include/NativeModules.hpp index 31d54f5477..862acfcf0c 100644 --- a/core/relinker/cli/include/NativeModules.hpp +++ b/core/relinker/cli/include/NativeModules.hpp @@ -16,6 +16,6 @@ bool SameDirectory(const std::filesystem::path& first, const std::filesystem::pa NativeModules ReadModules(const std::filesystem::path& inputPath, const std::filesystem::path& sceModulePath, const std::set& excludedModules, const std::vector& moduleDirectories); -void ReportUnconvertedNeeded(const NativeModules& modules, const std::vector& neededLibraries, std::ostream& out); +void ReportUnconvertedNeeded(const NativeModules& modules, const std::vector& neededLibraries, bool foldCase, std::ostream& out); } diff --git a/core/relinker/cli/src/NativeModules.cpp b/core/relinker/cli/src/NativeModules.cpp index a55ea22605..d10056fa28 100644 --- a/core/relinker/cli/src/NativeModules.cpp +++ b/core/relinker/cli/src/NativeModules.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -21,12 +22,12 @@ struct CandidateHeader { std::uint16_t Type = 0; }; -CandidateHeader ReadCandidateHeader(const std::filesystem::path& path) { +std::optional TryReadCandidateHeader(const std::filesystem::path& path) { std::ifstream stream(path, std::ios::binary); - if (!stream) throw Domain::RelinkerException("Cannot read guest candidate: " + Io::Utf8Path(path)); + if (!stream) return std::nullopt; char header[18]{}; stream.read(header, sizeof header); - if (stream.bad()) throw Domain::RelinkerException("Cannot read guest candidate magic: " + Io::Utf8Path(path)); + if (stream.bad()) return std::nullopt; CandidateHeader result; if (stream.gcount() < 4) return result; const auto byte = [&](const std::size_t index) { return static_cast(header[index]); }; @@ -36,6 +37,17 @@ CandidateHeader ReadCandidateHeader(const std::filesystem::path& path) { return result; } +CandidateHeader ReadCandidateHeader(const std::filesystem::path& path) { + const auto header = TryReadCandidateHeader(path); + if (!header) throw Domain::RelinkerException("Cannot read guest candidate: " + Io::Utf8Path(path)); + return *header; +} + +std::string FoldedName(std::string name, const bool foldCase) { + if (foldCase) for (auto& character : name) if (character >= 'A' && character <= 'Z') character = static_cast(character + ('a' - 'A')); + return name; +} + std::string_view ModuleStem(const std::string_view name) { return name.substr(0, name.size() - Domain::ModuleSuffix(name).size()); } @@ -130,10 +142,12 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil return result; } -void ReportUnconvertedNeeded(const NativeModules& modules, const std::vector& neededLibraries, std::ostream& out) { +void ReportUnconvertedNeeded(const NativeModules& modules, const std::vector& neededLibraries, const bool foldCase, std::ostream& out) { std::map> stems; - for (const auto& name : neededLibraries) - if (!Domain::ModuleSuffix(name).empty()) stems[std::string(ModuleStem(name))].push_back(name); + for (const auto& name : neededLibraries) { + const auto folded = FoldedName(name, foldCase); + if (!Domain::ModuleSuffix(folded).empty()) stems[std::string(ModuleStem(folded))].push_back(name); + } if (stems.empty()) return; struct Found { std::filesystem::path Relative; @@ -147,13 +161,13 @@ void ReportUnconvertedNeeded(const NativeModules& modules, const std::vectoris_regular_file()) continue; - const auto filename = Io::Utf8Path(entry->path().filename()); + const auto filename = FoldedName(Io::Utf8Path(entry->path().filename()), foldCase); if (Domain::ModuleSuffix(filename).empty()) continue; const auto stem = stems.find(std::string(ModuleStem(filename))); if (stem == stems.end()) continue; - const auto header = ReadCandidateHeader(entry->path()); - if (!header.Self && !(header.Elf && header.Type == 0xfe18)) continue; - for (const auto& name : stem->second) found.push_back({entry->path().lexically_relative(modules.Root), name, header.Self}); + const auto header = TryReadCandidateHeader(entry->path()); + if (!header || (!header->Self && !(header->Elf && header->Type == 0xfe18))) continue; + for (const auto& name : stem->second) found.push_back({entry->path().lexically_relative(modules.Root), name, header->Self}); } std::sort(found.begin(), found.end(), [](const auto& left, const auto& right) { return std::tie(left.Relative, left.Name) < std::tie(right.Relative, right.Name); }); for (const auto& [relative, name, self] : found) { diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 49ae7b38ac..904f129ad8 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -76,7 +76,7 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, progress.IncludeFile(Io::Utf8Path(input.OutputPath)); for (const auto& module : input.Modules) progress.IncludeFile(Io::Utf8Path(module.Path)); auto converted = Relinker::Conversion(progress).Convert(std::move(input), options); - if (!args.skipSceModule) Cli::ReportUnconvertedNeeded(modules, converted.Analysis.DynamicSection.NeededLibraries, std::cout); + if (!args.skipSceModule) Cli::ReportUnconvertedNeeded(modules, converted.Analysis.DynamicSection.NeededLibraries, args.toWindows, std::cout); return {inputPath, absPath, std::move(converted.Analysis), std::move(converted.Modules), std::move(converted.Executable), std::move(converted.WindowsDiagnostics), std::move(converted.AppliedPatches), converted.LinuxBase}; } diff --git a/core/relinker/relinker/tests/test_guest_needed_modules.py b/core/relinker/relinker/tests/test_guest_needed_modules.py index 99295fd68c..c6b04179dc 100644 --- a/core/relinker/relinker/tests/test_guest_needed_modules.py +++ b/core/relinker/relinker/tests/test_guest_needed_modules.py @@ -143,6 +143,24 @@ def check_needed_outside(relinker, work): (case / "needed.prx").write_bytes(sce_module) assert convert(case, ["--skip-sce-module"]) == [] + case = work / "outside-case" + (case / "sce_module").mkdir(parents=True) + (case / "Media").mkdir() + (case / "Media" / "NEEDED.PRX").write_bytes(sce_module) + assert convert(case) == [] + assert convert(case, ["--windows"]) == [message + "needed.prx at Media/NEEDED.PRX; add --module-dir Media"] + + if os.name != "nt" and os.geteuid() != 0: + case = work / "outside-unreadable" + (case / "sce_module").mkdir(parents=True) + (case / "Media").mkdir() + (case / "Media" / "needed.prx").write_bytes(sce_module) + (case / "needed.prx").write_bytes(self_container) + (case / "needed.prx").chmod(0) + try: + assert convert(case) == [message + "needed.prx at Media/needed.prx; add --module-dir Media"] + finally: + (case / "needed.prx").chmod(0o644) def main(): diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 20e78e83e2..c781c7ee49 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -27,7 +27,7 @@ relinker [options] An explicit `--module-dir` also works when the module parent has no `sce_module`, `sce_modules` or `prx` directory. For a homebrew bundle with shared libraries beside its executable, use `--module-dir .`; no empty standard directory is required. Without an explicit module directory, the standard-directory requirement remains. -A module the executable needs that is not converted is looked for under the module parent directory, outside the module directories. Each SELF container or `ET_SCE_DYNAMIC` ELF found there whose name matches, up to the `.prx`, `.sprx`, `.suprx` or `.debug_prx` suffix, is reported after the conversion with the `--module-dir` that would include it; the output still refers to a host library of that name. +A module the executable needs that is not converted is looked for under the module parent directory, outside the module directories. Each SELF container or `ET_SCE_DYNAMIC` ELF found there whose name matches, up to the `.prx`, `.sprx`, `.suprx` or `.debug_prx` suffix (ignoring case with `--windows`, as the guest module match does), is reported after the conversion with the `--module-dir` that would include it; the output still refers to a host library of that name. A file that cannot be read is not reported. Linux output: From 928094e49d6f97ff877b7ed5bcf3ca52a69f4b1f Mon Sep 17 00:00:00 2001 From: hypermist Date: Thu, 22 Oct 2026 21:37:53 +1300 Subject: [PATCH 077/428] test(libkernel_sys): pin the 20 firmware constant returns Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/CMakeLists.txt | 8 ++- core/libs/tests/GuestKernelSysConstants.cpp | 67 +++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 core/libs/tests/GuestKernelSysConstants.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index ef468b08aa..7c03104d74 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -315,12 +315,18 @@ if(BUILD_TESTING) configure_windows_unwind(guest_kernel_sys_thread_defaults_tests) add_test(NAME guest_kernel_sys_thread_defaults COMMAND guest_kernel_sys_thread_defaults_tests) set_tests_properties(guest_kernel_sys_thread_defaults PROPERTIES TIMEOUT 20) + add_test_executable(guest_kernel_sys_constants_tests tests/GuestKernelSysConstants.cpp) + target_include_directories(guest_kernel_sys_constants_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_constants_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_constants_tests) + add_test(NAME guest_kernel_sys_constants COMMAND guest_kernel_sys_constants_tests) + set_tests_properties(guest_kernel_sys_constants PROPERTIES TIMEOUT 20) if(Python3_Interpreter_FOUND) add_test(NAME nid_verneed COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_verneed.py $) endif() if(WIN32) - set_tests_properties(guest_kernel_sys guest_kernel_sys_thread_defaults PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_kernel_sys guest_kernel_sys_thread_defaults guest_kernel_sys_constants PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() if(APPLE) diff --git a/core/libs/tests/GuestKernelSysConstants.cpp b/core/libs/tests/GuestKernelSysConstants.cpp new file mode 100644 index 0000000000..418f98071c --- /dev/null +++ b/core/libs/tests/GuestKernelSysConstants.cpp @@ -0,0 +1,67 @@ +#include "SceTypes.hpp" +#include + +extern "C" { +int APS5_VABI pthread_switch_add_np_nid_postfix(void); +int APS5_VABI pthread_switch_delete_np_nid_postfix(void); +int APS5_VABI sceKernelAddResource(void); +int APS5_VABI sceKernelEventLogInit(void); +int APS5_VABI sceKernelIsGenuineKratosCex(void); +int APS5_VABI sceKernelReleaseTraceDirectMemory(void); +int APS5_VABI sceKernelHasNeoMode(void); +int APS5_VABI sceKernelIsAuthenticNeo(void); +int APS5_VABI sceKernelIsGenuineN(void); +int APS5_VABI sceKernelCreateEport(void); +int APS5_VABI sceKernelDeleteEport(void); +int APS5_VABI sceKernelOpenEport(void); +int APS5_VABI sceKernelCloseEport(void); +int APS5_VABI sceKernelTriggerEport(void); +int APS5_VABI sceKernelQueryTraceMemory(void); +int APS5_VABI sceKernelIccGetCPMode(void); +int APS5_VABI sceKernelIccSetCPMode(void); +int APS5_VABI sceKernelClearGameDirectMemory(void); +int APS5_VABI sceKernelMapTraceMemory(void); +int APS5_VABI sceKernelTraceMemoryTypeProtect(void); +} + +namespace { + +constexpr int GUEST_ENOTSUP = 45; +constexpr int SCE_KERNEL_ERROR_EPERM = static_cast(0x80020001); +constexpr int SCE_KERNEL_ERROR_ENOENT = static_cast(0x80020002); +constexpr int SCE_KERNEL_ERROR_ENOTSUP = static_cast(0x8002002d); +constexpr int SCE_KERNEL_ERROR_ENOSYS = static_cast(0x8002004e); + +void Require(bool value) { if (!value) std::abort(); } + +} + +int main() { + Require(pthread_switch_add_np_nid_postfix() == GUEST_ENOTSUP); + Require(pthread_switch_delete_np_nid_postfix() == GUEST_ENOTSUP); + + Require(sceKernelAddResource() == 0); + Require(sceKernelEventLogInit() == 0); + Require(sceKernelIsGenuineKratosCex() == 0); + Require(sceKernelReleaseTraceDirectMemory() == 0); + + Require(sceKernelHasNeoMode() == 1); + Require(sceKernelIsAuthenticNeo() == 1); + Require(sceKernelIsGenuineN() == 1); + + Require(sceKernelCreateEport() == SCE_KERNEL_ERROR_EPERM); + Require(sceKernelDeleteEport() == SCE_KERNEL_ERROR_EPERM); + Require(sceKernelOpenEport() == SCE_KERNEL_ERROR_EPERM); + Require(sceKernelCloseEport() == SCE_KERNEL_ERROR_EPERM); + Require(sceKernelTriggerEport() == SCE_KERNEL_ERROR_EPERM); + + Require(sceKernelQueryTraceMemory() == SCE_KERNEL_ERROR_ENOENT); + + Require(sceKernelIccGetCPMode() == SCE_KERNEL_ERROR_ENOTSUP); + Require(sceKernelIccSetCPMode() == SCE_KERNEL_ERROR_ENOTSUP); + + Require(sceKernelClearGameDirectMemory() == SCE_KERNEL_ERROR_ENOSYS); + Require(sceKernelMapTraceMemory() == SCE_KERNEL_ERROR_ENOSYS); + Require(sceKernelTraceMemoryTypeProtect() == SCE_KERNEL_ERROR_ENOSYS); + return 0; +} From e699e57ddb38a8f7dde7223d8b9964ede4c26c71 Mon Sep 17 00:00:00 2001 From: hypermist Date: Thu, 22 Oct 2026 22:37:28 +1300 Subject: [PATCH 078/428] fix(libScePsml): answer the MFSR2 calls as a console without PSSR does Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libScePsml_debug/Export.cpp | 66 +++++++++++++---------- core/libs/tests/GuestPsml.cpp | 48 +++++++++++++++++ docs/dev/debt/libScePsml_debug.md | 4 +- 3 files changed, 89 insertions(+), 29 deletions(-) diff --git a/core/libs/prx/libScePsml_debug/Export.cpp b/core/libs/prx/libScePsml_debug/Export.cpp index 44021762c2..420bb27a57 100644 --- a/core/libs/prx/libScePsml_debug/Export.cpp +++ b/core/libs/prx/libScePsml_debug/Export.cpp @@ -16,6 +16,8 @@ namespace { constexpr std::int32_t SCE_PSML_ERROR_NOT_INITIALIZED = static_cast(0x8A810001); constexpr std::int32_t SCE_PSML_ERROR_INVALID_ARGUMENT = static_cast(0x8A810005); constexpr std::int32_t SCE_PSML_ERROR_NULL_OUTPUT = static_cast(0x8A810009); +constexpr std::int32_t Mfsr2ErrorNotInitialized = static_cast(0x8A8A0001); +constexpr std::int32_t Mfsr2ErrorUnsupportedConsole = static_cast(0x8A8A0012); constexpr std::uint64_t MfsrBlockBytes = 0x200000; constexpr std::uint64_t MfsrBlockCount = 4; constexpr std::uint32_t MfsrDispatchPacketDwords = 0x600; @@ -209,6 +211,8 @@ void ReleaseMfsrObject(void* handle, MfsrObjectKind kind, const char* function) extern "C" { +int APS5_VABI sceKernelIsTrinityMode(void); + std::int32_t APS5_VABI scePsmlMfsrGetContextBufferRequirement1100(void* requirement, const void* param) { (void)requirement; (void)param; @@ -352,49 +356,57 @@ std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket900(void* context, void* return SCE_PSML_ERROR_NOT_INITIALIZED; } -int APS5_VABI scePsmlMfsr2ReleaseSharedResources(void* sharedResources) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI scePsmlMfsr2ReleaseSharedResources(void* sharedResources) { + (void)sharedResources; + return Mfsr2ErrorNotInitialized; } -int APS5_VABI scePsmlMfsr2GetSharedResourcesInitRequirement(MfsrRequirement* requirement, const Mfsr2SharedResourcesParam* param) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI scePsmlMfsr2GetSharedResourcesInitRequirement(MfsrRequirement* requirement, const Mfsr2SharedResourcesParam* param) { + (void)requirement; + (void)param; + return Mfsr2ErrorNotInitialized; } -int APS5_VABI scePsmlMfsr2GetContextInitRequirement(MfsrRequirement* requirement, const Mfsr2ContextParam* param) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI scePsmlMfsr2GetContextInitRequirement(MfsrRequirement* requirement, const Mfsr2ContextParam* param) { + (void)requirement; + (void)param; + return Mfsr2ErrorNotInitialized; } -int APS5_VABI scePsmlMfsr2ReleaseContext(void* context) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI scePsmlMfsr2ReleaseContext(void* context) { + (void)context; + return Mfsr2ErrorNotInitialized; } -int APS5_VABI scePsmlMfsr2GetDispatchPacketsSizeInDwords() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI scePsmlMfsr2GetDispatchPacketsSizeInDwords(void* context, std::uint32_t* packetSizeInDwords) { + (void)context; + (void)packetSizeInDwords; + return Mfsr2ErrorNotInitialized; } -int APS5_VABI scePsmlMfsr2CreateSharedResources(void** sharedResources, const Mfsr2SharedResourcesParam* param) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI scePsmlMfsr2CreateSharedResources(void** sharedResources, const Mfsr2SharedResourcesParam* param) { + (void)sharedResources; + (void)param; + return Mfsr2ErrorNotInitialized; } -int APS5_VABI scePsmlMfsr2GetDispatchPackets(void* context, const MfsrCommandBuffer* commandBuffer, const Mfsr2DispatchParam* param) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI scePsmlMfsr2GetDispatchPackets(void* context, const MfsrCommandBuffer* commandBuffer, const Mfsr2DispatchParam* param) { + (void)context; + (void)commandBuffer; + (void)param; + return Mfsr2ErrorNotInitialized; } -int APS5_VABI scePsmlMfsr2CreateContext(void** context, const Mfsr2ContextParam* param) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI scePsmlMfsr2CreateContext(void** context, const Mfsr2ContextParam* param) { + (void)context; + (void)param; + return Mfsr2ErrorNotInitialized; } -int APS5_VABI scePsmlMfsr2Init() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI scePsmlMfsr2Init() { + if (sceKernelIsTrinityMode() == 0) return Mfsr2ErrorUnsupportedConsole; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI scePsmlMfsrEnableSpecularAA() { diff --git a/core/libs/tests/GuestPsml.cpp b/core/libs/tests/GuestPsml.cpp index 882a4b5d04..5c009dbd13 100644 --- a/core/libs/tests/GuestPsml.cpp +++ b/core/libs/tests/GuestPsml.cpp @@ -27,6 +27,15 @@ std::int32_t APS5_VABI scePsmlMfsrSelectConfig(void* config); std::int32_t APS5_VABI scePsmlMfsrGetMipmapBias(void* context, float* bias, std::uint32_t dimension); std::int32_t APS5_VABI scePsmlMfsrRequestCapture(void* context); std::int32_t APS5_VABI scePsmlMfsrIsCaptureInProgress(void* context); +std::int32_t APS5_VABI scePsmlMfsr2Init(); +std::int32_t APS5_VABI scePsmlMfsr2GetSharedResourcesInitRequirement(void* requirement, const void* param); +std::int32_t APS5_VABI scePsmlMfsr2CreateSharedResources(void** sharedResources, const void* param); +std::int32_t APS5_VABI scePsmlMfsr2ReleaseSharedResources(void* sharedResources); +std::int32_t APS5_VABI scePsmlMfsr2GetContextInitRequirement(void* requirement, const void* param); +std::int32_t APS5_VABI scePsmlMfsr2CreateContext(void** context, const void* param); +std::int32_t APS5_VABI scePsmlMfsr2ReleaseContext(void* context); +std::int32_t APS5_VABI scePsmlMfsr2GetDispatchPacketsSizeInDwords(void* context, std::uint32_t* packetSizeInDwords); +std::int32_t APS5_VABI scePsmlMfsr2GetDispatchPackets(void* context, const void* commandBuffer, const void* param); } namespace { @@ -230,6 +239,44 @@ void SetupAfterInit() { Require(scePsmlMfsrReleaseSharedResources(sharedResources) == 0); } +void Mfsr2OnBaseConsole() { + constexpr std::int32_t notInitialized = static_cast(0x8A8A0001); + constexpr std::int32_t unsupportedConsole = static_cast(0x8A8A0012); + Require(scePsmlMfsr2Init() == unsupportedConsole); + Require(scePsmlMfsr2Init() == unsupportedConsole); + + std::array param{}; + std::array requirement; + requirement.fill(0xa5); + const auto untouchedRequirement = requirement; + Require(scePsmlMfsr2GetSharedResourcesInitRequirement(requirement.data(), param.data()) == notInitialized); + Require(scePsmlMfsr2GetContextInitRequirement(requirement.data(), param.data()) == notInitialized); + Require(requirement == untouchedRequirement); + + std::uint64_t storage = 0; + void* const sentinel = &storage; + void* handle = sentinel; + Require(scePsmlMfsr2CreateSharedResources(&handle, param.data()) == notInitialized); + Require(scePsmlMfsr2CreateContext(&handle, param.data()) == notInitialized); + Require(handle == sentinel); + Require(scePsmlMfsr2ReleaseContext(sentinel) == notInitialized); + Require(scePsmlMfsr2ReleaseSharedResources(sentinel) == notInitialized); + + std::uint32_t packetDwords = 0xa5a5a5a5; + Require(scePsmlMfsr2GetDispatchPacketsSizeInDwords(sentinel, &packetDwords) == notInitialized); + Require(packetDwords == 0xa5a5a5a5); + + std::array dispatchParam{}; + std::array dwords; + dwords.fill(0xa5a5a5a5u); + const auto untouchedDwords = dwords; + CommandBuffer commandBuffer{dwords.data(), static_cast(dwords.size())}; + Require(scePsmlMfsr2GetDispatchPackets(sentinel, &commandBuffer, dispatchParam.data()) == notInitialized); + Require(commandBuffer.cursor == dwords.data()); + Require(commandBuffer.sizeInDwords == dwords.size()); + Require(dwords == untouchedDwords); +} + void RefusedAfterInit() { SharedParam sharedParam{}; Requirement requirement{}; @@ -311,6 +358,7 @@ int main() { DispatchPacketUntouched(scePsmlMfsrGetDispatchMfsrPacket1000); DispatchPacketUntouched(scePsmlMfsrGetDispatchMfsrPacket1300); SetupBeforeInit(); + Mfsr2OnBaseConsole(); SetupAfterInit(); RefusedAfterInit(); } diff --git a/docs/dev/debt/libScePsml_debug.md b/docs/dev/debt/libScePsml_debug.md index f77c32d0b1..b056b06354 100644 --- a/docs/dev/debt/libScePsml_debug.md +++ b/docs/dev/debt/libScePsml_debug.md @@ -3,7 +3,7 @@ ### Unknown function info - [scePsmlMfsrGetSharedResourcesInitRequirement, scePsmlMfsrCreateSharedResources, scePsmlMfsrReleaseSharedResources, scePsmlMfsrGetContextBufferRequirement800M3_2, scePsmlMfsrCreateContext800M3_2, scePsmlMfsrReleaseContext](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - only resource type 0 is supported. The context requirement reports four 2 MiB blocks regardless of its parameters, while the system calculation depends on the context configuration. The descriptor pointer at 0x28 is not read. Validation throws for null pointers, short block lists and foreign handles instead of reproducing the system error and fault behaviour. -- [scePsmlMfsr2GetDispatchPacketsSizeInDwords](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - writes a 32-bit size through the second argument; the unused first argument still needs caller verification. +- [scePsmlMfsr2Init and the other MFSR2 calls](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - only a console without PSSR is modelled. In firmware 12.00 `libScePsml.sprx` (read from disassembly, not measured on a console), `scePsmlMfsr2Init` returns 0x8A8A0012 when `sceKernelIsTrinityMode` returns 0, and until `Init` succeeds every other MFSR2 call returns 0x8A8A0001 without touching its arguments. AnyPS5 reports 0, so `Init` throws only if that changes: the initialization after the check, the shared resource and context requirements, and the 0x800-dword size that `GetDispatchPacketsSizeInDwords` writes through its second argument are not implemented - [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; PPSA26344 imports but does not call the first two and at `0x87EFA` passes `(rdi, rsi=&stack, rdx)` as three pointers to `GetDispatchMfsrPacket1100`; before `scePsmlMfsrInit` they return 0x8A810001 (not initialized) and after it they throw. `scePsmlMfsrCreateContext1300` and `scePsmlMfsrGetDispatchMfsrPacket900`, `1000` and `1300` take the same shapes (context slot and parameter; context, command buffer and parameter) and behave the same way. PPSA08330 and PPSA03016 pass `GetDispatchMfsrPacket900`/`1000` the context handle, a `{cursor, size in dwords}` command buffer and the dispatch parameters, and advance their cursor by the size `scePsmlMfsrGetDispatchMfsrPacketSizeInDwords` returned. Whether the 1100 and 1300 versions take these arguments, and the console's error, are unverified - [scePsmlMfsrGetDispatchMfsrPacket1000](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - imported by PPSA25344. libScePsml and libScePsml_debug are built from the same source - [scePsmlMfsrCreateContext1300, scePsmlMfsrGetDispatchMfsrPacket1300](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - imported by PPSA28416 next to `scePsmlMfsrGetContextBufferRequirement1100`. The names are not in the aerolib NID list; they hash to the imported NIDs `kLF7WtQ1-Dg` and `DDVUyPyYHTA` (`tools/nid_names.py`) @@ -11,4 +11,4 @@ ### Functional - [scePsmlMfsrGetDispatchMfsrPacketSizeInDwords, scePsmlMfsrGetMipmapBias, scePsmlMfsrSelectConfig, scePsmlMfsrRequestCapture, scePsmlMfsrIsCaptureInProgress](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - semantics from the APR-EMU `fakelib/libScePsml.sprx`. Guards: `0x8A810001` uninitialized, `0x8A810005` null/foreign handle (fakelib checks an engine id `0xA9C4`/`0x9231`; our map check is the equivalent), `0x8A810009` null output. `GetDispatchMfsrPacketSizeInDwords` writes `0x600`; `GetMipmapBias(context, bias, dimension)` writes `log2f(dimension / context->inputWidth) - 1.0f`, width recorded at `CreateContext800M3_2` from the parameter pair at 0x18 or `descriptor0 + 0x20` for callers that pass a descriptor pointer there (indirection inferred; zero width returns `0x8A810005`). `SelectConfig`, `RequestCapture`, `IsCaptureInProgress` validate then return 0. The `GetDispatchMfsrPacket*` calls still throw: the 0x158-byte template's packet routine is not understood -- [scePsmlMfsrInit](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - succeeds and only sets the initialized state; the shared resources and contexts are bookkeeping objects that use none of the guest blocks. Whether Sony's library fails on hardware without PSSR is unknown. PPSA08330 calls the setup unconditionally and only uses a context when its cached `sceKernelIsTrinityMode` result is set (0x3c643d, 0x567f97; otherwise it stores a -0.25 mip bias itself at 0x567fe2), which AnyPS5 reports as 0 +- [scePsmlMfsrInit](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - succeeds and only sets the initialized state; the shared resources and contexts are bookkeeping objects that use none of the guest blocks. The console's library fails there instead: in firmware 12.00 (read from disassembly, not measured on a console) `scePsmlMfsrInit` returns 0x8A810012 when `sceKernelIsTrinityMode` returns 0, so its later MFSR calls return 0x8A810001. PPSA08330 calls the setup unconditionally and only uses a context when its cached `sceKernelIsTrinityMode` result is set (0x3c643d, 0x567f97; otherwise it stores a -0.25 mip bias itself at 0x567fe2), which AnyPS5 reports as 0 From 6e0c94cada77550ac0a6859dc2b40d321c34d94d Mon Sep 17 00:00:00 2001 From: hypermist Date: Thu, 22 Oct 2026 22:43:31 +1300 Subject: [PATCH 079/428] feat(libSceSpSysCallWrapper): forward 45 wrappers to the libkernel and libSceNet calls they jump to Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- .../prx/libSceSpSysCallWrapper/CMakeLists.txt | 16 ++ .../prx/libSceSpSysCallWrapper/Export.cpp | 271 +++++++++--------- core/libs/tests/GuestSpSysCallWrapper.cpp | 197 +++++++++++++ docs/dev/debt/libSceSpSysCallWrapper.md | 2 +- 4 files changed, 350 insertions(+), 136 deletions(-) create mode 100644 core/libs/prx/libSceSpSysCallWrapper/CMakeLists.txt create mode 100644 core/libs/tests/GuestSpSysCallWrapper.cpp diff --git a/core/libs/prx/libSceSpSysCallWrapper/CMakeLists.txt b/core/libs/prx/libSceSpSysCallWrapper/CMakeLists.txt new file mode 100644 index 0000000000..5e407e9a37 --- /dev/null +++ b/core/libs/prx/libSceSpSysCallWrapper/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceSpSysCallWrapper SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceSpSysCallWrapper PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceSpSysCallWrapper PRIVATE libSceNet) +configure_windows_unwind(libSceSpSysCallWrapper) + +if(BUILD_TESTING) + add_test_executable(guest_sp_syscall_wrapper_tests ${LIBS_INCLUDE_DIR}/tests/GuestSpSysCallWrapper.cpp) + target_include_directories(guest_sp_syscall_wrapper_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sp_syscall_wrapper_tests PRIVATE libSceSpSysCallWrapper libSceNet libkernel libc) + configure_windows_unwind(guest_sp_syscall_wrapper_tests) + add_test(NAME guest_sp_syscall_wrapper COMMAND guest_sp_syscall_wrapper_tests) + if(WIN32) + set_tests_properties(guest_sp_syscall_wrapper PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceSpSysCallWrapper/Export.cpp b/core/libs/prx/libSceSpSysCallWrapper/Export.cpp index e15c0a9054..deb4127d26 100644 --- a/core/libs/prx/libSceSpSysCallWrapper/Export.cpp +++ b/core/libs/prx/libSceSpSysCallWrapper/Export.cpp @@ -5,219 +5,223 @@ extern "C" { +int APS5_VABI sceKernelGettimeofday(KernelTimeval* tp); +int APS5_VABI sceKernelNanosleep(const KernelTimespec* rqtp, KernelTimespec* rmtp); +int APS5_VABI sceNetConnect(int s, const void* addr, std::uint32_t addrlen); +int APS5_VABI sceNetEpollAbort(int eid, int flags); +int APS5_VABI sceNetEpollControl(int eid, int op, int id, const void* event); +int APS5_VABI sceNetEpollCreate(const char* name, int flags); +int APS5_VABI sceNetEpollDestroy(int eid); +int APS5_VABI sceNetEpollWait(int eid, void* events, int maxevents, int timeout); +int* APS5_VABI sceNetErrnoLoc(void); +int APS5_VABI sceNetPoolCreate(const char* name, int size, int flags); +int APS5_VABI sceNetPoolDestroy(int memid); +std::int64_t APS5_VABI sceNetRecv(int s, void* buf, std::size_t len, int flags); +int APS5_VABI sceNetResolverAbort(int rid, int flags); +int APS5_VABI sceNetResolverCreate(const char* name, int memid, int flags); +int APS5_VABI sceNetResolverDestroy(int rid); +int APS5_VABI sceNetResolverGetError(int rid, int* status); +int APS5_VABI sceNetResolverStartNtoa(int rid, const char* hostname, void* addr, int timeout, int retry, int flags); +std::int64_t APS5_VABI sceNetSend(int s, const void* buf, std::size_t len, int flags); +int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval, std::uint32_t optlen); +int APS5_VABI sceNetSocket(const char* name, int family, int type, int protocol); +int APS5_VABI sceNetSocketClose(int s); +int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); +int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr* attr, int inheritSched); +int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param); +int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stackSize); +int APS5_VABI scePthreadCondDestroy(PthreadCond* cond); +int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, const char* name); +int APS5_VABI scePthreadCondSignal(PthreadCond* cond); +int APS5_VABI scePthreadCondWait(PthreadCond* cond, PthreadMutex* mutex); +int APS5_VABI scePthreadCondattrDestroy(PthreadCondattr* attr); +int APS5_VABI scePthreadCondattrInit(PthreadCondattr* attr); +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadDetach(Pthread thread); +void APS5_VABI scePthreadExit(void* retval); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char* name); +int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr); +int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr* attr); +int APS5_VABI scePthreadMutexattrSetprotocol(PthreadMutexattr* attr, int protocol); +int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type); +int APS5_VABI scePthreadSetprio(Pthread thread, int prio); + int APS5_VABI sceSpGetPid() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSpKernelGettimeofday() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpKernelGettimeofday(KernelTimeval* tp) { + return sceKernelGettimeofday(tp); } -int APS5_VABI sceSpKernelNanosleep() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpKernelNanosleep(const KernelTimespec* rqtp, KernelTimespec* rmtp) { + return sceKernelNanosleep(rqtp, rmtp); } -int APS5_VABI sceSpNetConnect() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetConnect(int s, const void* addr, std::uint32_t addrlen) { + return sceNetConnect(s, addr, addrlen); } -int APS5_VABI sceSpNetEpollAbort() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetEpollAbort(int eid, int flags) { + return sceNetEpollAbort(eid, flags); } -int APS5_VABI sceSpNetEpollControl() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetEpollControl(int eid, int op, int id, const void* event) { + return sceNetEpollControl(eid, op, id, event); } -int APS5_VABI sceSpNetEpollCreate() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetEpollCreate(const char* name, int flags) { + return sceNetEpollCreate(name, flags); } -int APS5_VABI sceSpNetEpollDestroy() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetEpollDestroy(int eid) { + return sceNetEpollDestroy(eid); } -int APS5_VABI sceSpNetEpollWait() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetEpollWait(int eid, void* events, int maxevents, int timeout) { + return sceNetEpollWait(eid, events, maxevents, timeout); } -int APS5_VABI sceSpNetErrnoLoc() { - NotImplemented_nid_no_patch(__func__); - return 0; +int* APS5_VABI sceSpNetErrnoLoc() { + return sceNetErrnoLoc(); } -int APS5_VABI sceSpNetPoolCreate() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetPoolCreate(const char* name, int size, int flags) { + return sceNetPoolCreate(name, size, flags); } -int APS5_VABI sceSpNetPoolDestroy() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetPoolDestroy(int memid) { + return sceNetPoolDestroy(memid); } -int APS5_VABI sceSpNetRecv() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceSpNetRecv(int s, void* buf, std::size_t len, int flags) { + return sceNetRecv(s, buf, len, flags); } -int APS5_VABI sceSpNetResolverAbort() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetResolverAbort(int rid, int flags) { + return sceNetResolverAbort(rid, flags); } -int APS5_VABI sceSpNetResolverCreate() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetResolverCreate(const char* name, int memid, int flags) { + return sceNetResolverCreate(name, memid, flags); } -int APS5_VABI sceSpNetResolverDestroy() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetResolverDestroy(int rid) { + return sceNetResolverDestroy(rid); } -int APS5_VABI sceSpNetResolverGetError() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetResolverGetError(int rid, int* status) { + return sceNetResolverGetError(rid, status); } -int APS5_VABI sceSpNetResolverStartNtoa() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetResolverStartNtoa(int rid, const char* hostname, void* addr, int timeout, int retry, int flags) { + return sceNetResolverStartNtoa(rid, hostname, addr, timeout, retry, flags); } -int APS5_VABI sceSpNetSend() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int64_t APS5_VABI sceSpNetSend(int s, const void* buf, std::size_t len, int flags) { + return sceNetSend(s, buf, len, flags); } -int APS5_VABI sceSpNetSetsockopt() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetSetsockopt(int s, int level, int optname, const void* optval, std::uint32_t optlen) { + return sceNetSetsockopt(s, level, optname, optval, optlen); } -int APS5_VABI sceSpNetSocket() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetSocket(const char* name, int family, int type, int protocol) { + return sceNetSocket(name, family, type, protocol); } -int APS5_VABI sceSpNetSocketClose() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpNetSocketClose(int s) { + return sceNetSocketClose(s); } -int APS5_VABI sceSpPthreadAttrDestroy() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadAttrDestroy(PthreadAttr* attr) { + return scePthreadAttrDestroy(attr); } -int APS5_VABI sceSpPthreadAttrInit() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadAttrInit(PthreadAttr* attr) { + return scePthreadAttrInit(attr); } -int APS5_VABI sceSpPthreadAttrSetinheritsched() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadAttrSetinheritsched(PthreadAttr* attr, int inheritSched) { + return scePthreadAttrSetinheritsched(attr, inheritSched); } -int APS5_VABI sceSpPthreadAttrSetschedparam() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param) { + return scePthreadAttrSetschedparam(attr, param); } -int APS5_VABI sceSpPthreadAttrSetstacksize() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stackSize) { + return scePthreadAttrSetstacksize(attr, stackSize); } -int APS5_VABI sceSpPthreadCondDestroy() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadCondDestroy(PthreadCond* cond) { + return scePthreadCondDestroy(cond); } -int APS5_VABI sceSpPthreadCondInit() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, const char* name) { + return scePthreadCondInit(cond, attr, name); } -int APS5_VABI sceSpPthreadCondSignal() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadCondSignal(PthreadCond* cond) { + return scePthreadCondSignal(cond); } -int APS5_VABI sceSpPthreadCondWait() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadCondWait(PthreadCond* cond, PthreadMutex* mutex) { + return scePthreadCondWait(cond, mutex); } -int APS5_VABI sceSpPthreadCondattrDestroy() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadCondattrDestroy(PthreadCondattr* attr) { + return scePthreadCondattrDestroy(attr); } -int APS5_VABI sceSpPthreadCondattrInit() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadCondattrInit(PthreadCondattr* attr) { + return scePthreadCondattrInit(attr); } -int APS5_VABI sceSpPthreadCreate() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name) { + return scePthreadCreate(thread, attr, entry, arg, name); } -int APS5_VABI sceSpPthreadDetach() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadDetach(Pthread thread) { + return scePthreadDetach(thread); } -int APS5_VABI sceSpPthreadExit() { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI sceSpPthreadExit(void* retval) { + scePthreadExit(retval); } -int APS5_VABI sceSpPthreadJoin() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadJoin(Pthread thread, void** retval) { + return scePthreadJoin(thread, retval); } -int APS5_VABI sceSpPthreadMutexDestroy() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadMutexDestroy(PthreadMutex* mutex) { + return scePthreadMutexDestroy(mutex); } -int APS5_VABI sceSpPthreadMutexInit() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char* name) { + return scePthreadMutexInit(mutex, attr, name); } -int APS5_VABI sceSpPthreadMutexLock() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadMutexLock(PthreadMutex* mutex) { + return scePthreadMutexLock(mutex); } -int APS5_VABI sceSpPthreadMutexUnlock() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadMutexUnlock(PthreadMutex* mutex) { + return scePthreadMutexUnlock(mutex); } -int APS5_VABI sceSpPthreadMutexattrDestroy() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadMutexattrDestroy(PthreadMutexattr* attr) { + return scePthreadMutexattrDestroy(attr); } -int APS5_VABI sceSpPthreadMutexattrInit() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadMutexattrInit(PthreadMutexattr* attr) { + return scePthreadMutexattrInit(attr); } int APS5_VABI sceSpPthreadMutexattrSetprioceiling() { @@ -225,19 +229,16 @@ int APS5_VABI sceSpPthreadMutexattrSetprioceiling() { return 0; } -int APS5_VABI sceSpPthreadMutexattrSetprotocol() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadMutexattrSetprotocol(PthreadMutexattr* attr, int protocol) { + return scePthreadMutexattrSetprotocol(attr, protocol); } -int APS5_VABI sceSpPthreadMutexattrSettype() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadMutexattrSettype(PthreadMutexattr* attr, int type) { + return scePthreadMutexattrSettype(attr, type); } -int APS5_VABI sceSpPthreadSetprio() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpPthreadSetprio(Pthread thread, int prio) { + return scePthreadSetprio(thread, prio); } int APS5_VABI sceSpSelect() { diff --git a/core/libs/tests/GuestSpSysCallWrapper.cpp b/core/libs/tests/GuestSpSysCallWrapper.cpp new file mode 100644 index 0000000000..80bce86a0d --- /dev/null +++ b/core/libs/tests/GuestSpSysCallWrapper.cpp @@ -0,0 +1,197 @@ +#include "SceTypes.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceSpKernelGettimeofday(KernelTimeval* tp); +int APS5_VABI sceSpKernelNanosleep(const KernelTimespec* rqtp, KernelTimespec* rmtp); +int APS5_VABI sceSpNetConnect(int s, const void* addr, std::uint32_t addrlen); +int APS5_VABI sceSpNetEpollAbort(int eid, int flags); +int APS5_VABI sceSpNetEpollControl(int eid, int op, int id, const void* event); +int APS5_VABI sceSpNetEpollCreate(const char* name, int flags); +int APS5_VABI sceSpNetEpollDestroy(int eid); +int APS5_VABI sceSpNetEpollWait(int eid, void* events, int maxevents, int timeout); +int* APS5_VABI sceSpNetErrnoLoc(); +int APS5_VABI sceSpNetPoolCreate(const char* name, int size, int flags); +int APS5_VABI sceSpNetPoolDestroy(int memid); +std::int64_t APS5_VABI sceSpNetRecv(int s, void* buf, std::size_t len, int flags); +int APS5_VABI sceSpNetResolverAbort(int rid, int flags); +int APS5_VABI sceSpNetResolverCreate(const char* name, int memid, int flags); +int APS5_VABI sceSpNetResolverDestroy(int rid); +int APS5_VABI sceSpNetResolverGetError(int rid, int* status); +std::int64_t APS5_VABI sceSpNetSend(int s, const void* buf, std::size_t len, int flags); +int APS5_VABI sceSpNetSetsockopt(int s, int level, int optname, const void* optval, std::uint32_t optlen); +int APS5_VABI sceSpNetSocket(const char* name, int family, int type, int protocol); +int APS5_VABI sceSpNetSocketClose(int s); +int APS5_VABI sceSpPthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI sceSpPthreadAttrInit(PthreadAttr* attr); +int APS5_VABI sceSpPthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stackSize); +int APS5_VABI sceSpPthreadCondDestroy(PthreadCond* cond); +int APS5_VABI sceSpPthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, const char* name); +int APS5_VABI sceSpPthreadCondSignal(PthreadCond* cond); +int APS5_VABI sceSpPthreadCondWait(PthreadCond* cond, PthreadMutex* mutex); +int APS5_VABI sceSpPthreadCondattrDestroy(PthreadCondattr* attr); +int APS5_VABI sceSpPthreadCondattrInit(PthreadCondattr* attr); +int APS5_VABI sceSpPthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI sceSpPthreadDetach(Pthread thread); +void APS5_VABI sceSpPthreadExit(void* retval); +int APS5_VABI sceSpPthreadJoin(Pthread thread, void** retval); +int APS5_VABI sceSpPthreadMutexDestroy(PthreadMutex* mutex); +int APS5_VABI sceSpPthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char* name); +int APS5_VABI sceSpPthreadMutexLock(PthreadMutex* mutex); +int APS5_VABI sceSpPthreadMutexUnlock(PthreadMutex* mutex); +int APS5_VABI sceSpPthreadMutexattrDestroy(PthreadMutexattr* attr); +int APS5_VABI sceSpPthreadMutexattrInit(PthreadMutexattr* attr); +int APS5_VABI sceSpPthreadMutexattrSettype(PthreadMutexattr* attr, int type); + +int* APS5_VABI sceNetErrnoLoc(); +int APS5_VABI sceNetConnect(int s, const void* addr, std::uint32_t addrlen); +int APS5_VABI sceNetEpollAbort(int eid, int flags); +int APS5_VABI sceNetEpollControl(int eid, int op, int id, const void* event); +int APS5_VABI sceNetEpollWait(int eid, void* events, int maxevents, int timeout); +std::int64_t APS5_VABI sceNetRecv(int s, void* buf, std::size_t len, int flags); +int APS5_VABI sceNetResolverAbort(int rid, int flags); +int APS5_VABI sceNetResolverGetError(int rid, int* status); +std::int64_t APS5_VABI sceNetSend(int s, const void* buf, std::size_t len, int flags); +int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval, std::uint32_t optlen); +int APS5_VABI sceSpNetResolverStartNtoa(int rid, const char* hostname, void* addr, int timeout, int retry, int flags); +int APS5_VABI sceSpPthreadAttrSetinheritsched(PthreadAttr* attr, int inheritSched); +int APS5_VABI sceSpPthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param); +int APS5_VABI sceSpPthreadMutexattrSetprotocol(PthreadMutexattr* attr, int protocol); +int APS5_VABI sceSpPthreadSetprio(Pthread thread, int prio); +int APS5_VABI sceNetResolverStartNtoa(int rid, const char* hostname, void* addr, int timeout, int retry, int flags); +int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr* attr, int inheritSched); +int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param); +int APS5_VABI scePthreadMutexattrSetprotocol(PthreadMutexattr* attr, int protocol); +int APS5_VABI scePthreadSetprio(Pthread thread, int prio); +Pthread APS5_VABI scePthreadSelf(); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +struct Shared { + PthreadMutex mutex = nullptr; + PthreadCond cond = nullptr; + bool ready = false; +}; + +void* APS5_VABI ExitWithArgument(void* arg) { + sceSpPthreadExit(arg); + return nullptr; +} + +void* APS5_VABI SignalReady(void* arg) { + auto* shared = static_cast(arg); + Require(sceSpPthreadMutexLock(&shared->mutex) == 0); + shared->ready = true; + Require(sceSpPthreadCondSignal(&shared->cond) == 0); + Require(sceSpPthreadMutexUnlock(&shared->mutex) == 0); + return nullptr; +} + +void Threads() { + PthreadAttr attr = nullptr; + Require(sceSpPthreadAttrInit(&attr) == 0); + Require(sceSpPthreadAttrSetstacksize(&attr, 0x10000) == 0); + Require(sceSpPthreadAttrSetinheritsched(&attr, 0) == scePthreadAttrSetinheritsched(&attr, 0)); + const KernelSchedParam param{700}; + Require(sceSpPthreadAttrSetschedparam(&attr, ¶m) == scePthreadAttrSetschedparam(&attr, ¶m)); + + Pthread thread = nullptr; + int marker = 0; + Require(sceSpPthreadCreate(&thread, &attr, ExitWithArgument, &marker, "sp_exit") == 0); + void* result = nullptr; + Require(sceSpPthreadJoin(thread, &result) == 0); + Require(result == &marker); + + Pthread detached = nullptr; + Require(sceSpPthreadCreate(&detached, &attr, ExitWithArgument, nullptr, "sp_detach") == 0); + Require(sceSpPthreadDetach(detached) == 0); + Require(sceSpPthreadAttrDestroy(&attr) == 0); + + PthreadMutexattr mutexAttr = nullptr; + Require(sceSpPthreadMutexattrInit(&mutexAttr) == 0); + Require(sceSpPthreadMutexattrSettype(&mutexAttr, 2) == 0); + Require(sceSpPthreadMutexattrSetprotocol(&mutexAttr, 0) == scePthreadMutexattrSetprotocol(&mutexAttr, 0)); + Require(sceSpPthreadSetprio(scePthreadSelf(), 700) == scePthreadSetprio(scePthreadSelf(), 700)); + PthreadCondattr condAttr = nullptr; + Require(sceSpPthreadCondattrInit(&condAttr) == 0); + + Shared shared; + Require(sceSpPthreadMutexInit(&shared.mutex, &mutexAttr, "sp_mutex") == 0); + Require(sceSpPthreadCondInit(&shared.cond, &condAttr, "sp_cond") == 0); + Require(sceSpPthreadMutexattrDestroy(&mutexAttr) == 0); + Require(sceSpPthreadCondattrDestroy(&condAttr) == 0); + + Require(sceSpPthreadMutexLock(&shared.mutex) == 0); + Pthread signaller = nullptr; + Require(sceSpPthreadCreate(&signaller, nullptr, SignalReady, &shared, "sp_signal") == 0); + while (!shared.ready) Require(sceSpPthreadCondWait(&shared.cond, &shared.mutex) == 0); + Require(sceSpPthreadMutexUnlock(&shared.mutex) == 0); + Require(sceSpPthreadJoin(signaller, nullptr) == 0); + Require(sceSpPthreadCondDestroy(&shared.cond) == 0); + Require(sceSpPthreadMutexDestroy(&shared.mutex) == 0); +} + +void Time() { + KernelTimeval now{}; + Require(sceSpKernelGettimeofday(&now) == 0); + Require(now.tv_sec > 0 && now.tv_usec >= 0 && now.tv_usec < 1000000); + const KernelTimespec delay{0, 1000000}; + Require(sceSpKernelNanosleep(&delay, nullptr) == 0); +} + +template +void SameError(TWrapped wrapped, TDirect direct) { + const auto wrappedResult = wrapped(); + const int wrappedErrno = *sceNetErrnoLoc(); + const auto directResult = direct(); + Require(wrappedResult == directResult); + Require(wrappedErrno == *sceNetErrnoLoc()); +} + +void Net() { + Require(sceSpNetErrnoLoc() == sceNetErrnoLoc()); + + const int pool = sceSpNetPoolCreate("sp_pool", 0x4000, 0); + Require(pool >= 0); + const int resolver = sceSpNetResolverCreate("sp_resolver", pool, 0); + Require(resolver >= 0); + int status = 0; + Require(sceSpNetResolverGetError(resolver, &status) == sceNetResolverGetError(resolver, &status)); + Require(sceSpNetResolverDestroy(resolver) == 0); + Require(sceSpNetPoolDestroy(pool) == 0); + + const int epoll = sceSpNetEpollCreate("sp_epoll", 0); + Require(epoll >= 0); + Require(sceSpNetEpollDestroy(epoll) == 0); + + const int socket = sceSpNetSocket("sp_socket", 2, 2, 0); + Require(socket >= 0); + const int enable = 1; + Require(sceSpNetSetsockopt(socket, 0xffff, 0x4, &enable, sizeof(enable)) == sceNetSetsockopt(socket, 0xffff, 0x4, &enable, sizeof(enable))); + Require(sceSpNetSocketClose(socket) == 0); + + char byte = 0; + const std::uint8_t address[16]{16, 2}; + SameError([&] { return sceSpNetSend(-1, &byte, 1, 0); }, [&] { return sceNetSend(-1, &byte, 1, 0); }); + SameError([&] { return sceSpNetRecv(-1, &byte, 1, 0); }, [&] { return sceNetRecv(-1, &byte, 1, 0); }); + SameError([&] { return sceSpNetConnect(-1, address, sizeof(address)); }, [&] { return sceNetConnect(-1, address, sizeof(address)); }); + SameError([&] { return sceSpNetEpollControl(-1, 1, 0, nullptr); }, [&] { return sceNetEpollControl(-1, 1, 0, nullptr); }); + SameError([&] { return sceSpNetEpollWait(-1, nullptr, 0, 0); }, [&] { return sceNetEpollWait(-1, nullptr, 0, 0); }); + SameError([&] { return sceSpNetEpollAbort(-1, 0); }, [&] { return sceNetEpollAbort(-1, 0); }); + SameError([&] { return sceSpNetResolverAbort(-1, 0); }, [&] { return sceNetResolverAbort(-1, 0); }); + std::uint32_t resolved = 0; + SameError([&] { return sceSpNetResolverStartNtoa(-1, "localhost", &resolved, 0, 0, 0); }, [&] { return sceNetResolverStartNtoa(-1, "localhost", &resolved, 0, 0, 0); }); +} + +} + +int main() { + Threads(); + Time(); + Net(); +} diff --git a/docs/dev/debt/libSceSpSysCallWrapper.md b/docs/dev/debt/libSceSpSysCallWrapper.md index ea6d5fbf4e..74d9011159 100644 --- a/docs/dev/debt/libSceSpSysCallWrapper.md +++ b/docs/dev/debt/libSceSpSysCallWrapper.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceSpGetPid, sceSpKernelGettimeofday, sceSpKernelNanosleep, sceSpNetConnect, sceSpNetEpollAbort, sceSpNetEpollControl, sceSpNetEpollCreate, sceSpNetEpollDestroy, sceSpNetEpollWait, sceSpNetErrnoLoc, sceSpNetPoolCreate, sceSpNetPoolDestroy, sceSpNetRecv, sceSpNetResolverAbort, sceSpNetResolverCreate, sceSpNetResolverDestroy, sceSpNetResolverGetError, sceSpNetResolverStartNtoa, sceSpNetSend, sceSpNetSetsockopt, sceSpNetSocket, sceSpNetSocketClose, sceSpPthreadAttrDestroy, sceSpPthreadAttrInit, sceSpPthreadAttrSetinheritsched, sceSpPthreadAttrSetschedparam, sceSpPthreadAttrSetstacksize, sceSpPthreadCondDestroy, sceSpPthreadCondInit, sceSpPthreadCondSignal, sceSpPthreadCondWait, sceSpPthreadCondattrDestroy, sceSpPthreadCondattrInit, sceSpPthreadCreate, sceSpPthreadDetach, sceSpPthreadExit, sceSpPthreadJoin, sceSpPthreadMutexDestroy, sceSpPthreadMutexInit, sceSpPthreadMutexLock, sceSpPthreadMutexUnlock, sceSpPthreadMutexattrDestroy, sceSpPthreadMutexattrInit, sceSpPthreadMutexattrSetprioceiling, sceSpPthreadMutexattrSetprotocol, sceSpPthreadMutexattrSettype, sceSpPthreadSetprio and sceSpSelect](../../../core/libs/prx/libSceSpSysCallWrapper/Export.cpp) (libSceSpSysCallWrapper) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSpSysCallWrapper.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceSpGetPid, sceSpSelect and sceSpPthreadMutexattrSetprioceiling](../../../core/libs/prx/libSceSpSysCallWrapper/Export.cpp) (libSceSpSysCallWrapper) - still throw. `sceSpGetPid` and `sceSpSelect` are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSpSysCallWrapper.c) but not exported by firmware 12.00's `libSceSpSysCallWrapper.sprx`, so no prototype could be read. There, `sceSpPthreadMutexattrSetprioceiling` only jumps to libkernel's `scePthreadMutexattrSetprioceiling`, which AnyPS5 does not implement yet (read from disassembly, not measured on a console) From 6116b70b04e71e5e367fff826dd91958f0deda6f Mon Sep 17 00:00:00 2001 From: hypermist Date: Thu, 22 Oct 2026 22:55:12 +1300 Subject: [PATCH 080/428] feat(libSceFont): build the graphics region and fill descriptors as the console does Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libSceFont/CMakeLists.txt | 9 + core/libs/prx/libSceFont/FontLibrary.cmake | 1 + .../prx/libSceFont/include/FontGraphics.hpp | 104 ++++++++ core/libs/prx/libSceFont/src/Graphics.cpp | 195 +++++++++++++++ .../libs/prx/libSceFont/src/Unimplemented.cpp | 113 ++------- core/libs/tests/GuestFontGraphics.cpp | 231 ++++++++++++++++++ docs/dev/debt/libSceFont.md | 2 +- 7 files changed, 557 insertions(+), 98 deletions(-) create mode 100644 core/libs/prx/libSceFont/include/FontGraphics.hpp create mode 100644 core/libs/prx/libSceFont/src/Graphics.cpp create mode 100644 core/libs/tests/GuestFontGraphics.cpp diff --git a/core/libs/prx/libSceFont/CMakeLists.txt b/core/libs/prx/libSceFont/CMakeLists.txt index cec16f879b..21847093a4 100644 --- a/core/libs/prx/libSceFont/CMakeLists.txt +++ b/core/libs/prx/libSceFont/CMakeLists.txt @@ -25,6 +25,15 @@ if(BUILD_TESTING) set_tests_properties(guest_font_writing_line PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_font_graphics_tests ${LIBS_INCLUDE_DIR}/tests/GuestFontGraphics.cpp) + target_include_directories(guest_font_graphics_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_font_graphics_tests PRIVATE libSceFont libc) + configure_windows_unwind(guest_font_graphics_tests) + add_test(NAME guest_font_graphics COMMAND guest_font_graphics_tests) + if(WIN32) + set_tests_properties(guest_font_graphics PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(font_face_lifetime_tests ${LIBS_INCLUDE_DIR}/tests/FontFaceLifetime.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/State.cpp) target_include_directories(font_face_lifetime_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceFont/FontLibrary.cmake b/core/libs/prx/libSceFont/FontLibrary.cmake index 47bf723165..975d86432e 100644 --- a/core/libs/prx/libSceFont/FontLibrary.cmake +++ b/core/libs/prx/libSceFont/FontLibrary.cmake @@ -1,6 +1,7 @@ function(add_sce_font_library target) set(fontDir ${CMAKE_CURRENT_FUNCTION_LIST_DIR}) add_library(${target} SHARED EXCLUDE_FROM_ALL + ${fontDir}/src/Graphics.cpp ${fontDir}/src/Layout.cpp ${fontDir}/src/Library.cpp ${fontDir}/src/Open.cpp diff --git a/core/libs/prx/libSceFont/include/FontGraphics.hpp b/core/libs/prx/libSceFont/include/FontGraphics.hpp new file mode 100644 index 0000000000..ecdeb72620 --- /dev/null +++ b/core/libs/prx/libSceFont/include/FontGraphics.hpp @@ -0,0 +1,104 @@ +#ifndef CORE_LIBS_PRX_LIBSCEFONT_INCLUDE_FONTGRAPHICS_HPP +#define CORE_LIBS_PRX_LIBSCEFONT_INCLUDE_FONTGRAPHICS_HPP + +#include +#include + +#include "prx/libc/include/General.hpp" + +struct FontGraphicsFill; + +struct FontGraphicsRegion { + float x; + float y; + float width; + float height; + std::uint32_t flags; + std::uint32_t tag; + float outerRadius; + float innerRadius; +}; + +static_assert(sizeof(FontGraphicsRegion) == 32); +static_assert(offsetof(FontGraphicsRegion, x) == 0); +static_assert(offsetof(FontGraphicsRegion, y) == 4); +static_assert(offsetof(FontGraphicsRegion, width) == 8); +static_assert(offsetof(FontGraphicsRegion, height) == 12); +static_assert(offsetof(FontGraphicsRegion, flags) == 16); +static_assert(offsetof(FontGraphicsRegion, tag) == 20); +static_assert(offsetof(FontGraphicsRegion, outerRadius) == 24); +static_assert(offsetof(FontGraphicsRegion, innerRadius) == 28); + +struct FontGraphicsFillMethod { + std::int32_t mode; + std::int32_t type; + std::uint32_t flags; + std::uint32_t tag; + std::byte reserved[16]; +}; + +static_assert(sizeof(FontGraphicsFillMethod) == 32); +static_assert(offsetof(FontGraphicsFillMethod, mode) == 0); +static_assert(offsetof(FontGraphicsFillMethod, type) == 4); +static_assert(offsetof(FontGraphicsFillMethod, flags) == 8); +static_assert(offsetof(FontGraphicsFillMethod, tag) == 12); +static_assert(offsetof(FontGraphicsFillMethod, reserved) == 16); + +struct FontGraphicsFillPlot { + float mapping[4]; + const FontGraphicsRegion* region; + std::uint16_t tag; + std::uint16_t reserved26; + std::uint16_t layout; + std::uint8_t flags; + std::byte reserved[33]; +}; + +static_assert(sizeof(FontGraphicsFillPlot) == 64); +static_assert(offsetof(FontGraphicsFillPlot, mapping) == 0); +static_assert(offsetof(FontGraphicsFillPlot, region) == 16); +static_assert(offsetof(FontGraphicsFillPlot, tag) == 24); +static_assert(offsetof(FontGraphicsFillPlot, reserved26) == 26); +static_assert(offsetof(FontGraphicsFillPlot, layout) == 28); +static_assert(offsetof(FontGraphicsFillPlot, flags) == 30); +static_assert(offsetof(FontGraphicsFillPlot, reserved) == 31); + +struct FontGraphicsFillRates { + float mapping[4]; + const FontGraphicsFill* fill; + std::uint16_t tag; + std::uint16_t effect; + std::uint16_t layout; + std::uint8_t flags; + std::byte reserved[33]; +}; + +static_assert(sizeof(FontGraphicsFillRates) == 64); +static_assert(offsetof(FontGraphicsFillRates, mapping) == 0); +static_assert(offsetof(FontGraphicsFillRates, fill) == 16); +static_assert(offsetof(FontGraphicsFillRates, tag) == 24); +static_assert(offsetof(FontGraphicsFillRates, effect) == 26); +static_assert(offsetof(FontGraphicsFillRates, layout) == 28); +static_assert(offsetof(FontGraphicsFillRates, flags) == 30); +static_assert(offsetof(FontGraphicsFillRates, reserved) == 31); + +namespace Font { + +using GraphicsErrorCallback = void (APS5_VABI*)(int error); + +void SetGraphicsErrorCallback(GraphicsErrorCallback callback); +int GraphicsRegionInit(FontGraphicsRegion* region, float x, float y, float width, float height); +int GraphicsRegionInitCircular(FontGraphicsRegion* region, float centerX, float centerY, float outerRadius, float innerRadius); +int GraphicsRegionInitRoundish(FontGraphicsRegion* region, float x, float y, float width, float height, float outerRadius, float innerRadius, std::uint32_t flags); +int GraphicsFillMethodInit(FontGraphicsFillMethod* method, std::int32_t mode, std::int32_t type); +int GraphicsFillPlotInit(FontGraphicsFillPlot* plot, const FontGraphicsRegion* region); +int GraphicsFillPlotSetLayout(FontGraphicsFillPlot* plot, std::int32_t first, std::int32_t second); +int GraphicsFillPlotSetMapping(FontGraphicsFillPlot* plot, float first, float second, float third, float fourth); +int GraphicsFillRatesInit(FontGraphicsFillRates* rates, const FontGraphicsFill* fill, std::uint32_t effect); +int GraphicsFillRatesSetFillEffect(FontGraphicsFillRates* rates, const FontGraphicsFill* fill, std::uint32_t effect); +int GraphicsFillRatesSetLayout(FontGraphicsFillRates* rates, std::int32_t first, std::int32_t second); +int GraphicsFillRatesSetMapping(FontGraphicsFillRates* rates, float first, float second, float third, float fourth); + +} + +#endif diff --git a/core/libs/prx/libSceFont/src/Graphics.cpp b/core/libs/prx/libSceFont/src/Graphics.cpp new file mode 100644 index 0000000000..8a005d478a --- /dev/null +++ b/core/libs/prx/libSceFont/src/Graphics.cpp @@ -0,0 +1,195 @@ +#include +#include + +#include "prx/libSceFont/include/FontGraphics.hpp" +#include "prx/libSceFont/include/FontTypes.hpp" + +namespace { + +constexpr int FONT_GRAPHICS_ERROR_INVALID_FILL = static_cast(0x80460086); +constexpr int FONT_GRAPHICS_ERROR_INVALID_REGION = static_cast(0x80460087); +constexpr int FONT_GRAPHICS_ERROR_INVALID_FILL_PLOT = static_cast(0x80460088); +constexpr int FONT_GRAPHICS_ERROR_INVALID_FILL_RATES = static_cast(0x80460089); + +constexpr std::uint32_t RegionTag = 0xf60; +constexpr std::uint32_t RectangleRegionFlags = 0xf; +constexpr std::uint32_t CircularRegionFlags = 0xff; +constexpr std::uint32_t FillMethodTag = 0xf01b; +constexpr std::uint32_t FillMethodFlags = 0xf; +constexpr std::uint16_t FillPlotTag = 0xf01c; +constexpr std::uint16_t FillRatesTag = 0xf01a; +constexpr std::uint16_t DefaultLayout = 0xf77; +constexpr std::uint32_t FillEffects = 0x1033f; + +std::atomic g_errorCallback{nullptr}; + +int Fail(int error) { + if (const auto callback = g_errorCallback.load()) callback(error); + return error; +} + +bool IsObject(const void* pointer) { + return reinterpret_cast(pointer) + 1 >= 2; +} + +bool IsValidFill(const FontGraphicsFill* fill) { + const auto value = reinterpret_cast(fill); + if (value >= -1 && value < 3) return true; + const std::uint8_t mode = *reinterpret_cast(fill); + return (mode & 0xf) == 0 && mode >= 0x10 && mode <= 0x50; +} + +bool IsValidEffect(std::uint32_t effect) { + return effect < 17 && ((FillEffects >> effect) & 1) != 0; +} + +bool IsValidLayoutNibble(std::uint32_t nibble) { + return nibble <= 4 && nibble != 3; +} + +bool IsValidLayout(std::int32_t first, std::int32_t second) { + const bool firstValid = first == 0 || first == 0x170 || first == 0x207 || first == 0x400 || first == 0x800 || first == 0xf77; + const auto bits = static_cast(second); + return firstValid && IsValidLayoutNibble((bits >> 4) & 0xf) && IsValidLayoutNibble(bits & 0xf); +} + +bool IsUsableRegion(const FontGraphicsRegion& region) { + if (region.tag != RegionTag) return false; + if (!(region.outerRadius >= 0.0f) || !(region.innerRadius >= 0.0f)) return false; + const float half = (region.width < region.height ? region.width : region.height) * 0.5f; + if (region.outerRadius == 0.0f) return half >= region.innerRadius; + return half >= region.outerRadius && region.outerRadius >= region.innerRadius; +} + +std::uint8_t MappedFlags(std::uint8_t flags) { + return static_cast((flags & 0xfc) | 1); +} + +} + +namespace Font { + +void SetGraphicsErrorCallback(GraphicsErrorCallback callback) { + g_errorCallback.store(callback); +} + +int GraphicsRegionInit(FontGraphicsRegion* region, float x, float y, float width, float height) { + if (!region || 0.0f > width || 0.0f > height) return Fail(SCE_FONT_ERROR_INVALID_PARAMETER); + *region = {x, y, width, height, RectangleRegionFlags, RegionTag, 0.0f, 0.0f}; + return SCE_FONT_OK; +} + +int GraphicsRegionInitCircular(FontGraphicsRegion* region, float centerX, float centerY, float outerRadius, float innerRadius) { + if (innerRadius > outerRadius || !region || 0.0f >= outerRadius || 0.0f >= innerRadius) return Fail(SCE_FONT_ERROR_INVALID_PARAMETER); + const float diameter = outerRadius + outerRadius; + *region = {centerX - outerRadius, centerY - outerRadius, diameter, diameter, CircularRegionFlags, RegionTag, outerRadius, innerRadius}; + return SCE_FONT_OK; +} + +int GraphicsRegionInitRoundish(FontGraphicsRegion* region, float x, float y, float width, float height, float outerRadius, float innerRadius, std::uint32_t flags) { + if (!region || 0.0f > innerRadius || 0.0f > outerRadius || 0.0f >= height || 0.0f >= width || flags > 0xff) return Fail(SCE_FONT_ERROR_INVALID_PARAMETER); + if (outerRadius > height * 0.5f || outerRadius > width * 0.5f) return Fail(SCE_FONT_ERROR_INVALID_PARAMETER); + if (outerRadius < innerRadius && outerRadius != 0.0f) return Fail(SCE_FONT_ERROR_INVALID_PARAMETER); + *region = {x, y, width, height, flags, RegionTag, outerRadius, innerRadius}; + return SCE_FONT_OK; +} + +int GraphicsFillMethodInit(FontGraphicsFillMethod* method, std::int32_t mode, std::int32_t type) { + const auto modeBits = static_cast(mode); + const bool modeValid = (modeBits & 0xf) == 0 && (modeBits >> 4) < 6; + const bool typeValid = static_cast(type) - 1 < 7 || type == 12; + if (!method || !modeValid || !typeValid) return Fail(SCE_FONT_ERROR_INVALID_PARAMETER); + *method = {}; + method->mode = mode; + method->type = type; + method->flags = FillMethodFlags; + method->tag = FillMethodTag; + return SCE_FONT_OK; +} + +int GraphicsFillPlotInit(FontGraphicsFillPlot* plot, const FontGraphicsRegion* region) { + if (!IsObject(plot)) return Fail(FONT_GRAPHICS_ERROR_INVALID_FILL_PLOT); + if (IsObject(region) && !IsUsableRegion(*region)) { + plot->tag = 0; + return Fail(FONT_GRAPHICS_ERROR_INVALID_REGION); + } + *plot = {}; + if (!region) { + plot->flags = 2; + } else if (!IsObject(region)) { + plot->flags = 6; + plot->layout = DefaultLayout; + } else { + plot->mapping[0] = region->x; + plot->mapping[1] = region->y; + plot->mapping[2] = region->x + region->width; + plot->mapping[3] = region->y + region->height; + plot->flags = 1; + plot->layout = DefaultLayout; + } + plot->region = region; + plot->tag = FillPlotTag; + return SCE_FONT_OK; +} + +int GraphicsFillPlotSetLayout(FontGraphicsFillPlot* plot, std::int32_t first, std::int32_t second) { + if (!IsObject(plot) || plot->tag != FillPlotTag) return Fail(FONT_GRAPHICS_ERROR_INVALID_FILL_PLOT); + if (!IsValidLayout(first, second)) return Fail(SCE_FONT_ERROR_INVALID_PARAMETER); + const auto combined = static_cast(first) | static_cast(second); + plot->layout = static_cast(combined); + plot->flags = static_cast((plot->flags & 0xf6) | (combined != 0 ? 1 : 0) | 8); + return SCE_FONT_OK; +} + +int GraphicsFillPlotSetMapping(FontGraphicsFillPlot* plot, float first, float second, float third, float fourth) { + if (!IsObject(plot) || plot->tag != FillPlotTag) return Fail(FONT_GRAPHICS_ERROR_INVALID_FILL_PLOT); + plot->mapping[0] = first; + plot->mapping[1] = second; + plot->mapping[2] = third; + plot->mapping[3] = fourth; + plot->flags = MappedFlags(plot->flags); + return SCE_FONT_OK; +} + +int GraphicsFillRatesInit(FontGraphicsFillRates* rates, const FontGraphicsFill* fill, std::uint32_t effect) { + if (!IsObject(rates)) return Fail(SCE_FONT_ERROR_INVALID_PARAMETER); + *rates = {}; + if (!IsValidFill(fill)) return Fail(FONT_GRAPHICS_ERROR_INVALID_FILL); + if (!IsValidEffect(effect)) return Fail(SCE_FONT_ERROR_INVALID_PARAMETER); + rates->fill = fill; + rates->tag = FillRatesTag; + rates->effect = static_cast(effect); + rates->layout = DefaultLayout; + rates->flags = 2; + return SCE_FONT_OK; +} + +int GraphicsFillRatesSetFillEffect(FontGraphicsFillRates* rates, const FontGraphicsFill* fill, std::uint32_t effect) { + if (!IsObject(rates) || rates->tag != FillRatesTag) return Fail(FONT_GRAPHICS_ERROR_INVALID_FILL_RATES); + if (!IsValidFill(fill)) return Fail(FONT_GRAPHICS_ERROR_INVALID_FILL); + if (!IsValidEffect(effect)) return Fail(SCE_FONT_ERROR_INVALID_PARAMETER); + rates->effect = static_cast(effect); + rates->fill = fill; + return SCE_FONT_OK; +} + +int GraphicsFillRatesSetLayout(FontGraphicsFillRates* rates, std::int32_t first, std::int32_t second) { + if (!IsObject(rates) || rates->tag != FillRatesTag) return Fail(FONT_GRAPHICS_ERROR_INVALID_FILL_RATES); + if (!IsValidLayout(first, second)) return Fail(SCE_FONT_ERROR_INVALID_PARAMETER); + const auto combined = static_cast(first) | static_cast(second); + rates->flags = static_cast((rates->flags & 0xfe) | (combined != 0 ? 1 : 0)); + rates->layout = 0; + return SCE_FONT_OK; +} + +int GraphicsFillRatesSetMapping(FontGraphicsFillRates* rates, float first, float second, float third, float fourth) { + if (!IsObject(rates) || rates->tag != FillRatesTag) return Fail(FONT_GRAPHICS_ERROR_INVALID_FILL_RATES); + rates->mapping[0] = first; + rates->mapping[1] = second; + rates->mapping[2] = third; + rates->mapping[3] = fourth; + rates->flags = MappedFlags(rates->flags); + return SCE_FONT_OK; +} + +} diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index c98c47a93e..c5e0a2a264 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "prx/libc/include/General.hpp" +#include "prx/libSceFont/include/FontGraphics.hpp" #include "prx/libSceFont/include/FontTypes.hpp" struct FontGraphicsDevice; @@ -22,81 +23,6 @@ struct FontGraphicsCanvas; struct FontGraphicsCanvasSequence; struct FontGraphicsSurface; struct FontGraphicsSurfaceTexture; -struct FontGraphicsFill; - -struct FontGraphicsRegion { - float x; - float y; - float width; - float height; - std::uint32_t flags; - std::uint32_t tag; - float outerRadius; - float innerRadius; -}; - -static_assert(sizeof(FontGraphicsRegion) == 32); -static_assert(offsetof(FontGraphicsRegion, x) == 0); -static_assert(offsetof(FontGraphicsRegion, y) == 4); -static_assert(offsetof(FontGraphicsRegion, width) == 8); -static_assert(offsetof(FontGraphicsRegion, height) == 12); -static_assert(offsetof(FontGraphicsRegion, flags) == 16); -static_assert(offsetof(FontGraphicsRegion, tag) == 20); -static_assert(offsetof(FontGraphicsRegion, outerRadius) == 24); -static_assert(offsetof(FontGraphicsRegion, innerRadius) == 28); - -struct FontGraphicsFillMethod { - std::int32_t mode; - std::int32_t type; - std::uint32_t flags; - std::uint32_t tag; - std::byte reserved[16]; -}; - -static_assert(sizeof(FontGraphicsFillMethod) == 32); -static_assert(offsetof(FontGraphicsFillMethod, mode) == 0); -static_assert(offsetof(FontGraphicsFillMethod, type) == 4); -static_assert(offsetof(FontGraphicsFillMethod, flags) == 8); -static_assert(offsetof(FontGraphicsFillMethod, tag) == 12); -static_assert(offsetof(FontGraphicsFillMethod, reserved) == 16); - -struct FontGraphicsFillPlot { - float mapping[4]; - const FontGraphicsRegion* region; - std::uint16_t tag; - std::uint16_t reserved26; - std::uint16_t layout; - std::uint8_t flags; - std::byte reserved[33]; -}; - -static_assert(sizeof(FontGraphicsFillPlot) == 64); -static_assert(offsetof(FontGraphicsFillPlot, mapping) == 0); -static_assert(offsetof(FontGraphicsFillPlot, region) == 16); -static_assert(offsetof(FontGraphicsFillPlot, tag) == 24); -static_assert(offsetof(FontGraphicsFillPlot, reserved26) == 26); -static_assert(offsetof(FontGraphicsFillPlot, layout) == 28); -static_assert(offsetof(FontGraphicsFillPlot, flags) == 30); -static_assert(offsetof(FontGraphicsFillPlot, reserved) == 31); - -struct FontGraphicsFillRates { - float mapping[4]; - const FontGraphicsFill* fill; - std::uint16_t tag; - std::uint16_t effect; - std::uint16_t layout; - std::uint8_t flags; - std::byte reserved[33]; -}; - -static_assert(sizeof(FontGraphicsFillRates) == 64); -static_assert(offsetof(FontGraphicsFillRates, mapping) == 0); -static_assert(offsetof(FontGraphicsFillRates, fill) == 16); -static_assert(offsetof(FontGraphicsFillRates, tag) == 24); -static_assert(offsetof(FontGraphicsFillRates, effect) == 26); -static_assert(offsetof(FontGraphicsFillRates, layout) == 28); -static_assert(offsetof(FontGraphicsFillRates, flags) == 30); -static_assert(offsetof(FontGraphicsFillRates, reserved) == 31); struct FontGraphicsPoolUsage { std::uint32_t capacity; @@ -240,43 +166,35 @@ int APS5_VABI sceFontGraphicsExchangeResource(FontGraphicsResource* resource, Fo } int APS5_VABI sceFontGraphicsFillMethodInit(FontGraphicsFillMethod* method, std::int32_t mode, std::int32_t type) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::GraphicsFillMethodInit(method, mode, type); } int APS5_VABI sceFontGraphicsFillPlotInit(FontGraphicsFillPlot* plot, const FontGraphicsRegion* region) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::GraphicsFillPlotInit(plot, region); } int APS5_VABI sceFontGraphicsFillPlotSetLayout(FontGraphicsFillPlot* plot, std::int32_t first, std::int32_t second) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::GraphicsFillPlotSetLayout(plot, first, second); } int APS5_VABI sceFontGraphicsFillPlotSetMapping(FontGraphicsFillPlot* plot, float first, float second, float third, float fourth) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::GraphicsFillPlotSetMapping(plot, first, second, third, fourth); } int APS5_VABI sceFontGraphicsFillRatesInit(FontGraphicsFillRates* rates, const FontGraphicsFill* fill, std::uint32_t effect) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::GraphicsFillRatesInit(rates, fill, effect); } int APS5_VABI sceFontGraphicsFillRatesSetFillEffect(FontGraphicsFillRates* rates, const FontGraphicsFill* fill, std::uint32_t effect) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::GraphicsFillRatesSetFillEffect(rates, fill, effect); } int APS5_VABI sceFontGraphicsFillRatesSetLayout(FontGraphicsFillRates* rates, std::int32_t first, std::int32_t second) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::GraphicsFillRatesSetLayout(rates, first, second); } int APS5_VABI sceFontGraphicsFillRatesSetMapping(FontGraphicsFillRates* rates, float first, float second, float third, float fourth) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::GraphicsFillRatesSetMapping(rates, first, second, third, fourth); } int APS5_VABI sceFontGraphicsGetDeviceUsage(FontGraphicsDevice* device, FontGraphicsDeviceUsage* usage) { @@ -285,18 +203,15 @@ int APS5_VABI sceFontGraphicsGetDeviceUsage(FontGraphicsDevice* device, FontGrap } int APS5_VABI sceFontGraphicsRegionInit(FontGraphicsRegion* region, float x, float y, float width, float height) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::GraphicsRegionInit(region, x, y, width, height); } int APS5_VABI sceFontGraphicsRegionInitCircular(FontGraphicsRegion* region, float centerX, float centerY, float outerRadius, float innerRadius) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::GraphicsRegionInitCircular(region, centerX, centerY, outerRadius, innerRadius); } int APS5_VABI sceFontGraphicsRegionInitRoundish(FontGraphicsRegion* region, float x, float y, float width, float height, float outerRadius, float innerRadius, std::uint32_t flags) { - NotImplemented_nid_no_patch(__func__); - return 0; + return Font::GraphicsRegionInitRoundish(region, x, y, width, height, outerRadius, innerRadius, flags); } int APS5_VABI sceFontGraphicsRelease(void** object) { @@ -464,6 +379,10 @@ int APS5_VABI sceFontGraphicsUpdateShapeFillPlot(FontGraphicsResource* resource, return 0; } +void APS5_VABI sceFontSetGraphicsErrorCallback(Font::GraphicsErrorCallback callback) { + Font::SetGraphicsErrorCallback(callback); +} + int APS5_VABI sceFontSupportGlyphs(const FontGlyphSupport* support) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestFontGraphics.cpp b/core/libs/tests/GuestFontGraphics.cpp new file mode 100644 index 0000000000..7129a32bcf --- /dev/null +++ b/core/libs/tests/GuestFontGraphics.cpp @@ -0,0 +1,231 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceFont/include/FontGraphics.hpp" +#include +#include +#include +#include +#include +#include + +using GraphicsErrorCallback = void (APS5_VABI*)(int error); + +extern "C" { +void APS5_VABI sceFontSetGraphicsErrorCallback(GraphicsErrorCallback callback); +int APS5_VABI sceFontGraphicsRegionInit(FontGraphicsRegion* region, float x, float y, float width, float height); +int APS5_VABI sceFontGraphicsRegionInitCircular(FontGraphicsRegion* region, float centerX, float centerY, float outerRadius, float innerRadius); +int APS5_VABI sceFontGraphicsRegionInitRoundish(FontGraphicsRegion* region, float x, float y, float width, float height, float outerRadius, float innerRadius, std::uint32_t flags); +int APS5_VABI sceFontGraphicsFillMethodInit(FontGraphicsFillMethod* method, std::int32_t mode, std::int32_t type); +int APS5_VABI sceFontGraphicsFillPlotInit(FontGraphicsFillPlot* plot, const FontGraphicsRegion* region); +int APS5_VABI sceFontGraphicsFillPlotSetLayout(FontGraphicsFillPlot* plot, std::int32_t first, std::int32_t second); +int APS5_VABI sceFontGraphicsFillPlotSetMapping(FontGraphicsFillPlot* plot, float first, float second, float third, float fourth); +int APS5_VABI sceFontGraphicsFillRatesInit(FontGraphicsFillRates* rates, const FontGraphicsFill* fill, std::uint32_t effect); +int APS5_VABI sceFontGraphicsFillRatesSetFillEffect(FontGraphicsFillRates* rates, const FontGraphicsFill* fill, std::uint32_t effect); +int APS5_VABI sceFontGraphicsFillRatesSetLayout(FontGraphicsFillRates* rates, std::int32_t first, std::int32_t second); +int APS5_VABI sceFontGraphicsFillRatesSetMapping(FontGraphicsFillRates* rates, float first, float second, float third, float fourth); +} + +namespace { + +constexpr int InvalidParameter = static_cast(0x80460002); +constexpr int InvalidFill = static_cast(0x80460086); +constexpr int InvalidRegion = static_cast(0x80460087); +constexpr int InvalidFillPlot = static_cast(0x80460088); +constexpr int InvalidFillRates = static_cast(0x80460089); +constexpr float NaN = std::numeric_limits::quiet_NaN(); + +int g_lastError = 0; +int g_errorCount = 0; + +void APS5_VABI RecordError(int error) { + g_lastError = error; + ++g_errorCount; +} + +void Require(bool value) { if (!value) std::abort(); } + +template +void Fill(T& object, std::uint8_t value) { + std::memset(&object, value, sizeof(object)); +} + +template +bool AllBytes(const T& object, std::size_t from, std::size_t to, std::uint8_t value) { + const auto* bytes = reinterpret_cast(&object); + for (std::size_t i = from; i < to; ++i) if (bytes[i] != value) return false; + return true; +} + +int g_expectedErrors = 0; + +void RequireError(int result, int error) { + Require(result == error); + Require(g_lastError == error); + g_lastError = 0; + ++g_expectedErrors; +} + +const FontGraphicsFill* Sentinel(std::intptr_t value) { + return reinterpret_cast(value); +} + +void Regions() { + FontGraphicsRegion region; + Fill(region, 0xa5); + Require(sceFontGraphicsRegionInit(®ion, 1.0f, 2.0f, 3.0f, 4.0f) == 0); + Require(region.x == 1.0f && region.y == 2.0f && region.width == 3.0f && region.height == 4.0f); + Require(region.flags == 0xf && region.tag == 0xf60 && region.outerRadius == 0.0f && region.innerRadius == 0.0f); + Require(sceFontGraphicsRegionInit(®ion, 0.0f, 0.0f, 0.0f, 0.0f) == 0); + Require(sceFontGraphicsRegionInit(®ion, 0.0f, 0.0f, NaN, NaN) == 0); + Fill(region, 0xa5); + RequireError(sceFontGraphicsRegionInit(®ion, 0.0f, 0.0f, -1.0f, 1.0f), InvalidParameter); + RequireError(sceFontGraphicsRegionInit(®ion, 0.0f, 0.0f, 1.0f, -1.0f), InvalidParameter); + RequireError(sceFontGraphicsRegionInit(nullptr, 0.0f, 0.0f, 1.0f, 1.0f), InvalidParameter); + Require(AllBytes(region, 0, sizeof(region), 0xa5)); + + Require(sceFontGraphicsRegionInitCircular(®ion, 10.0f, 20.0f, 4.0f, 1.0f) == 0); + Require(region.x == 6.0f && region.y == 16.0f && region.width == 8.0f && region.height == 8.0f); + Require(region.flags == 0xff && region.tag == 0xf60 && region.outerRadius == 4.0f && region.innerRadius == 1.0f); + Require(sceFontGraphicsRegionInitCircular(®ion, 0.0f, 0.0f, 2.0f, 2.0f) == 0); + RequireError(sceFontGraphicsRegionInitCircular(®ion, 0.0f, 0.0f, 1.0f, 2.0f), InvalidParameter); + RequireError(sceFontGraphicsRegionInitCircular(®ion, 0.0f, 0.0f, 0.0f, 0.0f), InvalidParameter); + RequireError(sceFontGraphicsRegionInitCircular(®ion, 0.0f, 0.0f, 1.0f, 0.0f), InvalidParameter); + RequireError(sceFontGraphicsRegionInitCircular(nullptr, 0.0f, 0.0f, 2.0f, 1.0f), InvalidParameter); + + Require(sceFontGraphicsRegionInitRoundish(®ion, 1.0f, 2.0f, 10.0f, 8.0f, 4.0f, 2.0f, 0x5) == 0); + Require(region.width == 10.0f && region.height == 8.0f && region.flags == 0x5 && region.tag == 0xf60); + Require(region.outerRadius == 4.0f && region.innerRadius == 2.0f); + Require(sceFontGraphicsRegionInitRoundish(®ion, 0.0f, 0.0f, 10.0f, 8.0f, 0.0f, 9.0f, 0xff) == 0); + RequireError(sceFontGraphicsRegionInitRoundish(®ion, 0.0f, 0.0f, 10.0f, 8.0f, 4.5f, 0.0f, 0), InvalidParameter); + RequireError(sceFontGraphicsRegionInitRoundish(®ion, 0.0f, 0.0f, 10.0f, 8.0f, 2.0f, 3.0f, 0), InvalidParameter); + RequireError(sceFontGraphicsRegionInitRoundish(®ion, 0.0f, 0.0f, 10.0f, 8.0f, 2.0f, 1.0f, 0x100), InvalidParameter); + RequireError(sceFontGraphicsRegionInitRoundish(®ion, 0.0f, 0.0f, 0.0f, 8.0f, 0.0f, 0.0f, 0), InvalidParameter); + RequireError(sceFontGraphicsRegionInitRoundish(®ion, 0.0f, 0.0f, 10.0f, 8.0f, -1.0f, 0.0f, 0), InvalidParameter); +} + +void FillMethods() { + FontGraphicsFillMethod method; + Fill(method, 0xa5); + Require(sceFontGraphicsFillMethodInit(&method, 0x50, 12) == 0); + Require(method.mode == 0x50 && method.type == 12 && method.flags == 0xf && method.tag == 0xf01b); + Require(AllBytes(method, 16, 32, 0)); + for (std::int32_t mode : {0x00, 0x10, 0x20, 0x30, 0x40}) Require(sceFontGraphicsFillMethodInit(&method, mode, 1) == 0); + for (std::int32_t type = 1; type <= 7; ++type) Require(sceFontGraphicsFillMethodInit(&method, 0, type) == 0); + for (std::int32_t mode : {0x01, 0x08, 0x60, -0x10}) RequireError(sceFontGraphicsFillMethodInit(&method, mode, 1), InvalidParameter); + for (std::int32_t type : {0, 8, 11, 13, -1}) RequireError(sceFontGraphicsFillMethodInit(&method, 0, type), InvalidParameter); + RequireError(sceFontGraphicsFillMethodInit(nullptr, 0, 1), InvalidParameter); +} + +void FillPlots() { + FontGraphicsRegion region; + Require(sceFontGraphicsRegionInitRoundish(®ion, 1.0f, 2.0f, 10.0f, 8.0f, 4.0f, 2.0f, 0x5) == 0); + FontGraphicsFillPlot plot; + Fill(plot, 0xa5); + Require(sceFontGraphicsFillPlotInit(&plot, ®ion) == 0); + Require(plot.mapping[0] == 1.0f && plot.mapping[1] == 2.0f && plot.mapping[2] == 11.0f && plot.mapping[3] == 10.0f); + Require(plot.region == ®ion && plot.tag == 0xf01c && plot.reserved26 == 0 && plot.layout == 0xf77 && plot.flags == 1); + Require(AllBytes(plot, 31, 64, 0)); + + Fill(plot, 0xa5); + Require(sceFontGraphicsFillPlotInit(&plot, nullptr) == 0); + Require(plot.region == nullptr && plot.tag == 0xf01c && plot.layout == 0 && plot.flags == 2); + Require(AllBytes(plot, 0, 16, 0)); + const auto* whole = reinterpret_cast(std::intptr_t{-1}); + Require(sceFontGraphicsFillPlotInit(&plot, whole) == 0); + Require(plot.region == whole && plot.layout == 0xf77 && plot.flags == 6); + + FontGraphicsRegion bad = region; + bad.tag = 0; + Fill(plot, 0xa5); + RequireError(sceFontGraphicsFillPlotInit(&plot, &bad), InvalidRegion); + Require(plot.tag == 0 && AllBytes(plot, 0, 24, 0xa5) && AllBytes(plot, 26, 64, 0xa5)); + bad = region; + bad.outerRadius = NaN; + RequireError(sceFontGraphicsFillPlotInit(&plot, &bad), InvalidRegion); + bad = region; + bad.outerRadius = 4.5f; + RequireError(sceFontGraphicsFillPlotInit(&plot, &bad), InvalidRegion); + bad = region; + bad.outerRadius = 0.0f; + bad.innerRadius = 4.0f; + Require(sceFontGraphicsFillPlotInit(&plot, &bad) == 0); + bad.innerRadius = 4.5f; + RequireError(sceFontGraphicsFillPlotInit(&plot, &bad), InvalidRegion); + RequireError(sceFontGraphicsFillPlotInit(nullptr, ®ion), InvalidFillPlot); + + Require(sceFontGraphicsFillPlotInit(&plot, ®ion) == 0); + Require(sceFontGraphicsFillPlotSetLayout(&plot, 0x400, 0x24) == 0); + Require(plot.layout == 0x424 && plot.flags == 9); + Require(sceFontGraphicsFillPlotSetLayout(&plot, 0, 0) == 0); + Require(plot.layout == 0 && plot.flags == 8); + Require(sceFontGraphicsFillPlotSetLayout(&plot, 0x170, 0x1100) == 0); + Require(plot.layout == 0x1170); + RequireError(sceFontGraphicsFillPlotSetLayout(&plot, 0x171, 0), InvalidParameter); + RequireError(sceFontGraphicsFillPlotSetLayout(&plot, 0, 0x3), InvalidParameter); + RequireError(sceFontGraphicsFillPlotSetLayout(&plot, 0, 0x50), InvalidParameter); + Require(sceFontGraphicsFillPlotSetMapping(&plot, 5.0f, 6.0f, 7.0f, NaN) == 0); + Require(plot.mapping[0] == 5.0f && plot.mapping[2] == 7.0f && std::isnan(plot.mapping[3]) && plot.flags == 9); + + FontGraphicsFillPlot untagged{}; + RequireError(sceFontGraphicsFillPlotSetLayout(&untagged, 0, 0), InvalidFillPlot); + RequireError(sceFontGraphicsFillPlotSetMapping(&untagged, 0.0f, 0.0f, 0.0f, 0.0f), InvalidFillPlot); +} + +void FillRates() { + FontGraphicsFillRates rates; + Fill(rates, 0xa5); + std::array fill{0x30}; + const auto* fillPointer = reinterpret_cast(fill.data()); + Require(sceFontGraphicsFillRatesInit(&rates, fillPointer, 9) == 0); + Require(rates.fill == fillPointer && rates.tag == 0xf01a && rates.effect == 9 && rates.layout == 0xf77 && rates.flags == 2); + Require(AllBytes(rates, 0, 16, 0) && AllBytes(rates, 31, 64, 0)); + for (std::intptr_t value : {-1, 0, 1, 2}) Require(sceFontGraphicsFillRatesInit(&rates, Sentinel(value), 0) == 0); + for (std::uint32_t effect : {0u, 1u, 2u, 3u, 4u, 5u, 8u, 9u, 16u}) { + Require(sceFontGraphicsFillRatesInit(&rates, nullptr, effect) == 0); + Require(rates.effect == effect); + } + + Fill(rates, 0xa5); + RequireError(sceFontGraphicsFillRatesInit(&rates, nullptr, 6), InvalidParameter); + Require(AllBytes(rates, 0, sizeof(rates), 0)); + fill[0] = 0x31; + RequireError(sceFontGraphicsFillRatesInit(&rates, fillPointer, 0), InvalidFill); + fill[0] = 0x60; + RequireError(sceFontGraphicsFillRatesInit(&rates, fillPointer, 0), InvalidFill); + RequireError(sceFontGraphicsFillRatesInit(nullptr, nullptr, 0), InvalidParameter); + + fill[0] = 0x10; + Require(sceFontGraphicsFillRatesInit(&rates, nullptr, 0) == 0); + Require(sceFontGraphicsFillRatesSetFillEffect(&rates, fillPointer, 16) == 0); + Require(rates.fill == fillPointer && rates.effect == 16 && rates.flags == 2 && rates.layout == 0xf77); + RequireError(sceFontGraphicsFillRatesSetFillEffect(&rates, nullptr, 17), InvalidParameter); + Require(rates.fill == fillPointer && rates.effect == 16); + + Require(sceFontGraphicsFillRatesSetLayout(&rates, 0xf77, 0x44) == 0); + Require(rates.layout == 0 && rates.flags == 3); + Require(sceFontGraphicsFillRatesSetLayout(&rates, 0, 0) == 0); + Require(rates.layout == 0 && rates.flags == 2); + RequireError(sceFontGraphicsFillRatesSetLayout(&rates, 0x801, 0), InvalidParameter); + Require(sceFontGraphicsFillRatesSetMapping(&rates, 1.0f, 2.0f, 3.0f, 4.0f) == 0); + Require(rates.mapping[0] == 1.0f && rates.mapping[3] == 4.0f && rates.flags == 1); + + FontGraphicsFillRates untagged{}; + RequireError(sceFontGraphicsFillRatesSetFillEffect(&untagged, nullptr, 0), InvalidFillRates); + RequireError(sceFontGraphicsFillRatesSetLayout(&untagged, 0, 0), InvalidFillRates); + RequireError(sceFontGraphicsFillRatesSetMapping(&untagged, 0.0f, 0.0f, 0.0f, 0.0f), InvalidFillRates); +} + +} + +int main() { + sceFontSetGraphicsErrorCallback(RecordError); + Regions(); + FillMethods(); + FillPlots(); + FillRates(); + Require(g_errorCount == g_expectedErrors); + sceFontSetGraphicsErrorCallback(nullptr); + const int before = g_errorCount; + FontGraphicsRegion region; + Require(sceFontGraphicsRegionInit(®ion, 0.0f, 0.0f, -1.0f, 0.0f) == InvalidParameter); + Require(g_errorCount == before); +} diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index b1b5e56398..b62da4ea2b 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -24,7 +24,7 @@ - [Font graphics and metric signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): graphics-device/service creation takes font memory, parameters and a handle output; destruction takes a handle pointer. Attribute selectors and outputs are 32-bit values. Metric, outline-profile, style-information and expansion-buffer outputs remain opaque pending field recovery. Protected implementations are unchanged. -- [Font graphics fill and transform signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): region layout and scalar float arguments are confirmed from assembly, including the extra 32-bit corner flags omitted by the roundish-region decompilation. Fill-method, plot and rates layouts, layout selector meanings and mapping parameter meanings remain unresolved. Protected bodies are unchanged. +- [Font graphics fill descriptors](../../../core/libs/prx/libSceFont/src/Graphics.cpp): the region, fill-method, fill-plot and fill-rates initialisers and setters, and `sceFontSetGraphicsErrorCallback`, follow firmware 12.00 `libSceFont.sprx` (read from disassembly, not measured on a console): argument checks in the same order, error codes, tags and the bytes written. What the layout selectors, mapping values, region corner flags and fill effects mean is unknown, because the calls that read these descriptors (`sceFontGraphicsSetup*`, `sceFontGraphicsUpdate*`) still throw. The error codes `0x80460086` to `0x80460089` have no public names; the source names them after the argument they reject Frame lifecycle, drawing completion, device usage, frame policy, clipping and fill setup/update argument signatures in [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered. Frame and resource layouts remain opaque; their runtime implementation is still unavailable. From d85426069c66e0a8314b5d42731bb04f2c87f6b7 Mon Sep 17 00:00:00 2001 From: dantdmdarealguy Date: Wed, 21 Oct 2026 21:05:31 -0500 Subject: [PATCH 081/428] fix(tools): decode Ninja's header dependencies as UTF-8 in select_tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ninja -t deps lists the paths GCC wrote to its depfiles. WinLibs GCC 15.2.0 runs with the UTF-8 active code page, while its Ninja 1.13.2 reports an ANSI build-file encoding, so a header under C:\Users\José\... decoded with mbcs became José, fell outside the source tree and dropped out of every test's inputs. Decode the dependency lines as UTF-8 and keep the build-file encoding for the outputs they belong to. --- tools/select_tests.py | 9 +++++---- tools/tests/test_select_tests.py | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/tools/select_tests.py b/tools/select_tests.py index efb972d0a1..08c715d642 100644 --- a/tools/select_tests.py +++ b/tools/select_tests.py @@ -125,11 +125,12 @@ def chunks(names): def dump(build, out): deps = collections.defaultdict(set) current = None - for line in build.run("-t", "deps").splitlines(): - if not line.startswith(" ") and ": #deps" in line: - current = build.key(line.split(": #deps")[0]) + recorded = subprocess.run([build.program, "-C", build.path, "-t", "deps"], capture_output=True, check=True).stdout + for line in recorded.splitlines(): + if not line.startswith(b" ") and b": #deps" in line: + current = build.key(line.split(b": #deps")[0].decode(build.encoding)) elif line.strip() and current: - path = build.repo(line.strip()) + path = build.repo(line.strip().decode("utf-8")) if path: deps[current].add(path) listing, listed = build.tests() diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py index e86b16ccad..8b9a7873f6 100644 --- a/tools/tests/test_select_tests.py +++ b/tools/tests/test_select_tests.py @@ -207,6 +207,31 @@ class FixtureBuildTests(unittest.TestCase): self.assertIn(name, output) +@unittest.skipUnless(shutil.which("ninja") and (shutil.which("gcc") or shutil.which("cc")), "needs ninja and a C compiler") +class NonAsciiDirectoryTests(unittest.TestCase): + def test_header_under_a_non_ascii_directory_is_an_input_of_its_tests(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) / "ação" + if os.name == "nt": + try: + str(root).encode("mbcs", "strict") + except UnicodeEncodeError: + self.skipTest("the ANSI code page cannot represent the fixture directory") + source, build, graph = root / "source", root / "build", root / "graph.json" + for name, content in PROJECT.items(): + (source / name).parent.mkdir(parents=True, exist_ok=True) + (source / name).write_text(content, encoding="utf-8") + compiler = shutil.which("gcc") or shutil.which("cc") + subprocess.run(["cmake", "-S", str(source), "-B", str(build), "-G", "Ninja", f"-DCMAKE_C_COMPILER={compiler}"], + check=True, capture_output=True) + subprocess.run(["cmake", "--build", str(build)], check=True, capture_output=True) + SelectTests.tool("dump", build, graph) + reasons = {} + select_tests.select(json.loads(graph.read_text(encoding="utf-8")), ["alpha/alpha.h"], reasons) + self.assertIn("input: alpha/alpha.h", reasons["alpha"]) + self.assertIn("input: alpha/alpha.h", reasons["alpha_data"]) + + class BuildErrorTests(unittest.TestCase): def run_tool(self, *args): return subprocess.run([sys.executable, "-I", str(TOOL), *map(str, args)], capture_output=True, text=True, timeout=60) From c5dd9c1dd66191abaa10a127a84cf5fd61ae4c22 Mon Sep 17 00:00:00 2001 From: Enrique V <105763896+EnriqueDev2002@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:49:36 -0600 Subject: [PATCH 082/428] docs(build): add macOS relinker prerequisites Name the commands that provide the compiler, CMake and Ninja on macOS next to the existing mention of AppleClang in the relinker-only section. Co-Authored-By: Claude Fable 5.1 --- docs/dev/BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 1b1a1287ac..e4f24bcadd 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -12,7 +12,7 @@ ctest --test-dir build-relinker --output-on-failure Python must be found during CMake configuration for Python tests to be registered. List registered tests with `ctest --test-dir build-relinker -N` and look for `input_magic`, a Python-backed relinker test. If automatic discovery fails, add `-DPython3_EXECUTABLE="C:/path/to/python.exe"` to the configure command, using the path to your Python interpreter, then list the tests again. -This mode builds the conversion tool and its tests on Linux, Windows and macOS, including Apple Silicon. macOS uses AppleClang from the Xcode command-line tools. Windows uses MinGW-w64 GCC with C++20 support and accepts distributions with static or shared runtimes; the full build also requires the exception support described below. The executable is `build-relinker/core/relinker/relinker` (`relinker.exe` on Windows with Ninja). +This mode builds the conversion tool and its tests on Linux, Windows and macOS, including Apple Silicon. macOS uses AppleClang from the Xcode command-line tools (`xcode-select --install`); CMake and Ninja come from Homebrew (`brew install cmake ninja`). Windows uses MinGW-w64 GCC with C++20 support and accepts distributions with static or shared runtimes; the full build also requires the exception support described below. The executable is `build-relinker/core/relinker/relinker` (`relinker.exe` on Windows with Ninja). The output is x86-64: Linux ELF by default, Windows PE with `--windows` or Mach-O with `--macos` ([usage](../user/USAGE.md#input-and-conversion)). Converted games need system libraries built for the target OS and a compatible x86-64 host. This mode does not build those libraries, and releases ship them for Linux and Windows only. Tests inspect all three output formats; execution checks run only on their compatible hosts. From 89f99c2f505e8a079e095c95040158d71d668824 Mon Sep 17 00:00:00 2001 From: Itamar Medyoni Date: Wed, 21 Oct 2026 23:56:59 +0300 Subject: [PATCH 083/428] fix(libkernel): fail fchmod on a pipe with EINVAL like FreeBSD fchmod and sceKernelFchmod on either end of a pipe answered ENOENT on Windows, where the pipe has no path, and changed the pipe's mode on Linux. FreeBSD 11's pipe_chmod sends an unnamed pipe to invfo_chmod, which returns EINVAL, as sockets already get here. --- core/libs/prx/libkernel/File/src/Stdio.cpp | 1 + core/libs/tests/GuestFilesystem.cpp | 2 ++ 2 files changed, 3 insertions(+) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 2aece3834c..39366cb16c 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -1499,6 +1499,7 @@ int APS5_VABI sceKernelChmod_nid_postfix(const char* path, std::uint16_t mode) { int APS5_VABI sceKernelFchmod(int d, std::uint16_t mode) { if (GuestSockets::IsOpen(d)) return SceErrorFromErrno(GUEST_EINVAL); + if (IsPipeDescriptor(d)) return SceErrorFromErrno(GUEST_EINVAL); File::RefuseArchivedFile(d, "fchmod"); if (NativeFchmod(d, mode & 07777) != 0) return SceErrorFromErrno(errno); return 0; diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 1557080c12..bab9fca5e0 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -919,6 +919,8 @@ int main() { Require(fsync_nid_postfix(end) == -1 && *__error_nid_postfix() == 22); Require(fdatasync_nid_postfix(end) == -1 && *__error_nid_postfix() == 22); Require(sceKernelFsync(end) == static_cast(0x80020016u) && sceKernelFdatasync(end) == static_cast(0x80020016u)); + Require(fchmod_nid_postfix(end, 0600) == -1 && *__error_nid_postfix() == 22); + Require(sceKernelFchmod(end, 0600) == static_cast(0x80020016u)); } Require(close_nid_postfix(syncPipe[0]) == 0 && close_nid_postfix(syncPipe[1]) == 0); Require(close_nid_postfix(socket) == 0); From fcf6dece996ac07a179992129c826c29cbe7c2c4 Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Thu, 22 Oct 2026 01:38:14 +0000 Subject: [PATCH 084/428] fix(libSceAjm.native): guard LibAtrac9 BitReverse32 and SignExtend32 against undefined shifts in the ATRAC9 decoder --- core/libs/prx/libSceAjm.native/CMakeLists.txt | 4 ++-- .../prx/libSceAjm.native/PatchLibAtrac9.cmake | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAjm.native/CMakeLists.txt b/core/libs/prx/libSceAjm.native/CMakeLists.txt index c69ccc483c..9ebf52d600 100644 --- a/core/libs/prx/libSceAjm.native/CMakeLists.txt +++ b/core/libs/prx/libSceAjm.native/CMakeLists.txt @@ -2,7 +2,7 @@ enable_language(C) set(LIBATRAC9_DIR ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) set(LIBATRAC9_PATCHED_DIR ${CMAKE_CURRENT_BINARY_DIR}/atrac9) -set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${LIBATRAC9_DIR}/unpack.c ${LIBATRAC9_DIR}/bit_reader.c ${LIBATRAC9_DIR}/decinit.c) +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${LIBATRAC9_DIR}/unpack.c ${LIBATRAC9_DIR}/bit_reader.c ${LIBATRAC9_DIR}/decinit.c ${LIBATRAC9_DIR}/utility.c) include(${CMAKE_CURRENT_SOURCE_DIR}/PatchLibAtrac9.cmake) add_library(atrac9 STATIC EXCLUDE_FROM_ALL ${LIBATRAC9_DIR}/band_extension.c @@ -17,7 +17,7 @@ add_library(atrac9 STATIC EXCLUDE_FROM_ALL ${LIBATRAC9_DIR}/scale_factors.c ${LIBATRAC9_DIR}/tables.c ${LIBATRAC9_PATCHED_DIR}/unpack.c - ${LIBATRAC9_DIR}/utility.c + ${LIBATRAC9_PATCHED_DIR}/utility.c ) target_include_directories(atrac9 PRIVATE ${LIBATRAC9_DIR}) set_target_properties(atrac9 PROPERTIES POSITION_INDEPENDENT_CODE ON) diff --git a/core/libs/prx/libSceAjm.native/PatchLibAtrac9.cmake b/core/libs/prx/libSceAjm.native/PatchLibAtrac9.cmake index 6c1b91c006..ed0a64c6e9 100644 --- a/core/libs/prx/libSceAjm.native/PatchLibAtrac9.cmake +++ b/core/libs/prx/libSceAjm.native/PatchLibAtrac9.cmake @@ -28,3 +28,21 @@ if(libatrac9ConfigGuardPosition EQUAL -1) endif() string(REPLACE "${libatrac9OldConfigGuard}" "${libatrac9NewConfigGuard}" libatrac9Decinit "${libatrac9Decinit}") file(WRITE ${LIBATRAC9_PATCHED_DIR}/decinit.c "${libatrac9Decinit}") + +file(READ ${LIBATRAC9_DIR}/utility.c libatrac9Utility) +set(libatrac9OldReverse "\treturn value >> (32 - bitCount);") +set(libatrac9NewReverse "\treturn bitCount ? value >> (32 - bitCount) : 0u;") +string(FIND "${libatrac9Utility}" "${libatrac9OldReverse}" libatrac9ReversePosition) +if(libatrac9ReversePosition EQUAL -1) + message(FATAL_ERROR "LibAtrac9 BitReverse32 has changed; review the local shift-by-32 guard") +endif() +string(REPLACE "${libatrac9OldReverse}" "${libatrac9NewReverse}" libatrac9Utility "${libatrac9Utility}") + +set(libatrac9OldSignExtend "\treturn (value << shift) >> shift;") +set(libatrac9NewSignExtend "\treturn (int)((unsigned int)value << shift) >> shift;") +string(FIND "${libatrac9Utility}" "${libatrac9OldSignExtend}" libatrac9SignExtendPosition) +if(libatrac9SignExtendPosition EQUAL -1) + message(FATAL_ERROR "LibAtrac9 SignExtend32 has changed; review the local signed-shift guard") +endif() +string(REPLACE "${libatrac9OldSignExtend}" "${libatrac9NewSignExtend}" libatrac9Utility "${libatrac9Utility}") +file(WRITE ${LIBATRAC9_PATCHED_DIR}/utility.c "${libatrac9Utility}") From bb89d0ae6510abacd7e5e1f5b837ddd680629cea Mon Sep 17 00:00:00 2001 From: harisibrar1 <207127467+harisibrar1@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:20:48 -0400 Subject: [PATCH 085/428] build(relinker): give dlltool a short temporary prefix for the msvcrt import library dlltool names its temporary assembler files after the output path and creates them in the build directory, so the windows_stdio_msvcrt_import.a step fails with "failed to open temporary head file" once that file name plus the directory exceeds MAX_PATH: with a build directory of 96 characters or more. Every other target still builds there. Pass --temp-prefix so the temporary files keep a fixed short name in the same directory. Co-Authored-By: Claude Opus 5.5 --- core/relinker/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 31560112a0..bbc5cd2435 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -649,7 +649,7 @@ if(BUILD_TESTING) set(msvcrtImportLibrary "${CMAKE_CURRENT_BINARY_DIR}/windows_stdio_msvcrt_import.a") add_custom_command(OUTPUT "${msvcrtImportLibrary}" COMMAND "${mingwDlltoolExecutable}" -d "${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/WindowsMsvcrt.def" - -l "${msvcrtImportLibrary}" -D msvcrt.dll + -l "${msvcrtImportLibrary}" -D msvcrt.dll --temp-prefix windows_stdio_msvcrt_import DEPENDS elfpatcher/tests/WindowsMsvcrt.def VERBATIM) add_custom_target(windows_stdio_msvcrt_import DEPENDS "${msvcrtImportLibrary}") From 6b1916a2edf869d3553aee815890fa65e5e878c0 Mon Sep 17 00:00:00 2001 From: harisibrar1 <207127467+harisibrar1@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:51:39 -0400 Subject: [PATCH 086/428] test(relinker): decode relinker output as UTF-8 in the path-checking tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The relinker writes paths to stdout, stderr and its patch manifest as UTF-8, but seven test scripts read them with the locale encoding. On Windows that is the ANSI code page, so with a build directory such as C:/Users/José/AnyPS5/build (the tests keep their temporary files under the build tree) the decoded paths no longer match and game_patches, guest_needed_modules, guest_symbol_names, input_file, input_preservation, relinker_executables and tls_diagnostics fail. Co-Authored-By: Claude Opus 5.5 --- .../relinker/tests/test_executables.py | 2 +- .../relinker/tests/test_game_patches.py | 8 ++++---- .../tests/test_guest_needed_modules.py | 10 +++++----- .../relinker/tests/test_guest_symbol_names.py | 10 +++++----- .../relinker/tests/test_input_file.py | 2 +- .../relinker/tests/test_input_preservation.py | 20 +++++++++---------- .../relinker/tests/test_tls_diagnostics.py | 2 +- 7 files changed, 27 insertions(+), 27 deletions(-) diff --git a/core/relinker/relinker/tests/test_executables.py b/core/relinker/relinker/tests/test_executables.py index 844c33eb10..b58acc9317 100644 --- a/core/relinker/relinker/tests/test_executables.py +++ b/core/relinker/relinker/tests/test_executables.py @@ -27,7 +27,7 @@ def main(): output = case / "out" / ("app.exe" if windows else "app.elf") output.parent.mkdir(exist_ok=True) result = subprocess.run([str(relinker), *(["--windows"] if windows else []), "--registry", *options, str(source), str(output)], - capture_output=True, text=True, timeout=60) + capture_output=True, text=True, encoding="utf-8", timeout=60) return result, output for windows in (False, True): diff --git a/core/relinker/relinker/tests/test_game_patches.py b/core/relinker/relinker/tests/test_game_patches.py index 4fb2fd064a..960ea15692 100644 --- a/core/relinker/relinker/tests/test_game_patches.py +++ b/core/relinker/relinker/tests/test_game_patches.py @@ -36,7 +36,7 @@ def main(): work = Path(directory) def run(*arguments): - return subprocess.run([str(relinker), *arguments], capture_output=True, text=True, timeout=60) + return subprocess.run([str(relinker), *arguments], capture_output=True, text=True, encoding="utf-8", timeout=60) def case(name): root = work / name @@ -64,7 +64,7 @@ def main(): assert f"Patch: eboot.bin [flip_bit] va={address:#x} off=" in result.stdout, result.stdout assert f"{expected:02x} -> {expected ^ 1:02x} (flip one bit)\n" in result.stdout, result.stdout manifest = output.parent / "app.patches.toml" - text = manifest.read_text() + text = manifest.read_text(encoding="utf-8") assert 'main_target = "eboot.bin"' in text and 'name = "flip_bit"' in text, text assert 'main_target = "eboot.bin"\nlinux_base = 0x400000\n' in text, text assert f"Patch manifest: {manifest}\n" in result.stdout @@ -154,7 +154,7 @@ def main(): def relocation_overlap(relinker, work): def run(*arguments): - return subprocess.run([str(relinker), *arguments], capture_output=True, text=True, timeout=60) + return subprocess.run([str(relinker), *arguments], capture_output=True, text=True, encoding="utf-8", timeout=60) def patch_file(path, target, address, expected): path.write_text(f'format = 1\n\n[[patch]]\ntarget = "{target}"\nva = {address:#x}\nexpected = "{expected}"\nvalue = "ffff"\n') @@ -196,7 +196,7 @@ def relocation_overlap(relinker, work): def writer_overlap(relinker, work): def run(*arguments): - return subprocess.run([str(relinker), *arguments], capture_output=True, text=True, timeout=60) + return subprocess.run([str(relinker), *arguments], capture_output=True, text=True, encoding="utf-8", timeout=60) def attempt(name, module, options, address, expected, value="ff", target="sce_module/sample.prx"): case = work / name diff --git a/core/relinker/relinker/tests/test_guest_needed_modules.py b/core/relinker/relinker/tests/test_guest_needed_modules.py index 6b2b18f2c8..35739524aa 100644 --- a/core/relinker/relinker/tests/test_guest_needed_modules.py +++ b/core/relinker/relinker/tests/test_guest_needed_modules.py @@ -63,7 +63,7 @@ def check_guest_needed(relinker, work): (case / "prx" / "consumer.prx").write_bytes(module_with_needed(names, entries)) output = case / ("output.exe" if windows else "output.elf") result = subprocess.run([str(relinker), *(["--windows"] if windows else []), str(source), str(output)], - capture_output=True, text=True, timeout=30) + capture_output=True, text=True, encoding="utf-8", timeout=30) assert result.returncode == 0, (label, windows, result.stdout, result.stderr) artifact = case / "app0" / "prx" / "consumer.prx.guest.prx" assert artifact.read_bytes().startswith(b"MZ" if windows else b"\x7fELF"), artifact @@ -81,7 +81,7 @@ def check_guest_needed(relinker, work): module.write_bytes(module_with_needed((first, invalid), (0, 0, 1))) output = case / ("output.exe" if windows else "output.elf") result = subprocess.run([str(relinker), *(["--windows"] if windows else []), str(source), str(output)], - capture_output=True, text=True, timeout=30) + capture_output=True, text=True, encoding="utf-8", timeout=30) assert result.returncode == 2 and f"{module}: Invalid dependency: {invalid.decode()}\n" in result.stderr, result.stderr assert not output.exists() and not (case / "app0").exists(), output @@ -99,7 +99,7 @@ def main(): source.write_bytes(executable_with_needed(needed, repeats)) output = case / ("output.exe" if windows else "output.elf") result = subprocess.run([str(relinker), *(["--windows"] if windows else []), str(source), str(output)], - capture_output=True, text=True, timeout=30) + capture_output=True, text=True, encoding="utf-8", timeout=30) return result, output for windows in (False, True): @@ -118,7 +118,7 @@ def main(): assert layout in result.stdout, result.stdout for spelling in (os.path.join(".", output.name), os.path.join("..", case.name, output.name)): relative = subprocess.run([str(relinker), *(["--windows"] if windows else []), "input.elf", spelling], cwd=case, - capture_output=True, text=True, timeout=30) + capture_output=True, text=True, encoding="utf-8", timeout=30) assert relative.returncode == 0 and layout in relative.stdout, (spelling, relative.stdout, relative.stderr) if windows and os.name == "nt": run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) @@ -203,7 +203,7 @@ def main(): source = case / "input.elf" source.write_bytes(executable_with_needed(b"libSceVideoOut.prx", 2)) result = subprocess.run([str(relinker), "--macos", str(source), str(case / "output")], - capture_output=True, text=True, timeout=30) + capture_output=True, text=True, encoding="utf-8", timeout=30) assert result.returncode == 0, (result.stdout, result.stderr) print("Guest needed module tests passed") diff --git a/core/relinker/relinker/tests/test_guest_symbol_names.py b/core/relinker/relinker/tests/test_guest_symbol_names.py index 3580b83e32..e41660d237 100644 --- a/core/relinker/relinker/tests/test_guest_symbol_names.py +++ b/core/relinker/relinker/tests/test_guest_symbol_names.py @@ -148,7 +148,7 @@ def relink(relinker, case, executable, modules): for name, image in modules.items(): (case / "sce_module" / name).write_bytes(image) output = case / "output.elf" - result = subprocess.run([str(relinker), str(source), str(output)], capture_output=True, text=True, timeout=30) + result = subprocess.run([str(relinker), str(source), str(output)], capture_output=True, text=True, encoding="utf-8", timeout=30) assert result.returncode == 0, (result.stdout, result.stderr) return output @@ -192,7 +192,7 @@ def main(): for name, image in twins.items(): (work / "needed-both" / "sce_module" / name).write_bytes(image) result = subprocess.run([str(relinker), str(work / "needed-both" / "input.elf"), str(work / "needed-both" / "output.elf")], - capture_output=True, text=True, timeout=30) + capture_output=True, text=True, encoding="utf-8", timeout=30) assert result.returncode != 0 and "Ambiguous guest import AAAAAAAAAAA" in result.stderr, (result.stdout, result.stderr) guests = relink(relinker, work / "guest-declared", importing_executable([]), { @@ -222,7 +222,7 @@ def main(): needed_module_import("YYYYYYYYYYY", "XXXXXXXXXXX#A#B", "libc.prx", (tag,))) output = case / ("output.exe" if windows else "output.elf") result = subprocess.run([str(relinker), *(["--windows"] if windows else []), str(case / "input.elf"), str(output)], - capture_output=True, text=True, timeout=30) + capture_output=True, text=True, encoding="utf-8", timeout=30) assert result.returncode == 0, (windows, hex(tag), result.stdout, result.stderr) if not windows: modules = case / "app0" / "sce_module" @@ -240,7 +240,7 @@ def main(): module = case / "sce_module" / "libc.prx" module.write_bytes(needed_module_import("XXXXXXXXXXX", "YYYYYYYYYYY#A#B", "libkernel.prx", (0x6100000F, 0x61000045))) result = subprocess.run([str(relinker), str(case / "input.elf"), str(case / "output.elf")], - capture_output=True, text=True, timeout=30) + capture_output=True, text=True, encoding="utf-8", timeout=30) assert result.returncode == 2 and f"{module}: Guest module mixes PS4 and PS5 needed-module tags\n" in result.stderr, result.stderr assert not (case / "output.elf").exists() @@ -250,7 +250,7 @@ def main(): module = case / "sce_module" / "libc.prx" module.write_bytes(needed_module_import("XXXXXXXXXXX", "YYYYYYYYYYY#A#C", "libkernel.prx")) result = subprocess.run([str(relinker), str(case / "input.elf"), str(case / "output.elf")], - capture_output=True, text=True, timeout=30) + capture_output=True, text=True, encoding="utf-8", timeout=30) assert result.returncode == 2 and "Unknown import module ID: YYYYYYYYYYY#A#C\n" in result.stderr \ and f"Input: {module}\n" in result.stderr, result.stderr assert not (case / "output.elf").exists() diff --git a/core/relinker/relinker/tests/test_input_file.py b/core/relinker/relinker/tests/test_input_file.py index 6146103953..2124c7395e 100644 --- a/core/relinker/relinker/tests/test_input_file.py +++ b/core/relinker/relinker/tests/test_input_file.py @@ -21,7 +21,7 @@ def main(): output = work / (name + ".out") registry = work / (name + ".registry.json") result = subprocess.run([str(relinker), "--skip-sce-module", "--registry", *flags, str(source), str(output)], - capture_output=True, text=True, timeout=20) + capture_output=True, text=True, encoding="utf-8", timeout=20) assert result.returncode == 2 and error in result.stderr and "bad_alloc" not in result.stderr, (name, flags, result) assert not output.exists() and not registry.exists(), (name, flags, result) diff --git a/core/relinker/relinker/tests/test_input_preservation.py b/core/relinker/relinker/tests/test_input_preservation.py index 241dd1f4bc..4fb96462f0 100644 --- a/core/relinker/relinker/tests/test_input_preservation.py +++ b/core/relinker/relinker/tests/test_input_preservation.py @@ -30,7 +30,7 @@ def check_executable_hardlinks(relinker, work): links = protected.stat().st_nlink try: result = subprocess.run([str(relinker), *command], cwd=case, - capture_output=True, text=True, timeout=20) + capture_output=True, text=True, encoding="utf-8", timeout=20) assert result.returncode == 2 and "output would overwrite" in result.stderr, ( windows, output_name, input_name, readonly, result.stdout, result.stderr) for path, original in inputs.items(): @@ -61,7 +61,7 @@ def check_executable_hardlinks(relinker, work): path.chmod(stat.S_IREAD) try: result = subprocess.run([str(relinker), *command], cwd=case, - capture_output=True, text=True, timeout=20) + capture_output=True, text=True, encoding="utf-8", timeout=20) assert result.returncode == 0, (readonly, result.stdout, result.stderr) for name in ("executable", "secondary-one", "secondary-two"): assert outputs[name].read_bytes().startswith(b"MZ") @@ -111,7 +111,7 @@ def check_diagnostic_inputs(relinker, work): options.append("--registry") result = subprocess.run([str(relinker), "--windows", "--windows-diagnostics", *options, str(source), str(output)], cwd=diagnostics.parent, - capture_output=True, text=True, timeout=20) + capture_output=True, text=True, encoding="utf-8", timeout=20) owner = "an input module" if bundled else "the input executable" assert result.returncode == 2 and f"Windows import diagnostics output would overwrite {owner}" in result.stderr, (result.stdout, result.stderr) assert str(diagnostics) in result.stderr, result.stderr @@ -134,7 +134,7 @@ def check_diagnostic_inputs(relinker, work): options = ["--windows-diagnostics"] if enabled else [] result = subprocess.run([str(relinker), "--windows", "--skip-sce-module", *options, str(source), str(output)], cwd=case, - capture_output=True, text=True, timeout=20) + capture_output=True, text=True, encoding="utf-8", timeout=20) assert result.returncode == 0 and output.read_bytes().startswith(b"MZ"), (result.stdout, result.stderr) assert source.read_bytes() == original assert diagnostics.read_bytes() == (b"" if enabled else original) @@ -177,7 +177,7 @@ def check_registry_inputs(relinker, work, windows, needed=False): previous_output = b"previous executable" output.write_bytes(previous_output) result = subprocess.run([str(relinker), *options, "--registry", str(source), str(output)], - capture_output=True, text=True, timeout=20) + capture_output=True, text=True, encoding="utf-8", timeout=20) kind = "Needed libraries" if needed else "Registry" expected = f"{kind} output would overwrite an input module" if bundled else f"{kind} output would overwrite the input executable" assert result.returncode == 2 and expected in result.stderr, (result.returncode, result.stdout, result.stderr) @@ -204,7 +204,7 @@ def check_registry_after_failed_patch(relinker, work): for registry in registries: registry.write_bytes(previous_registry) result = subprocess.run([str(relinker), "--windows", "--registry", str(source), str(output)], - capture_output=True, text=True, timeout=20) + capture_output=True, text=True, encoding="utf-8", timeout=20) assert result.returncode == 2 and "Invalid Windows icon PNG" in result.stderr, (result.stdout, result.stderr) assert output.read_bytes() == previous_output assert all(registry.read_bytes() == previous_registry for registry in registries) @@ -225,7 +225,7 @@ def check_output_directory(relinker, work): output = str(target) if existing else str(target) + os.sep options = ["--windows"] if windows else [] result = subprocess.run([str(relinker), *options, str(source), output], cwd=case, - capture_output=True, text=True, timeout=20) + capture_output=True, text=True, encoding="utf-8", timeout=20) assert result.returncode == 2 and "Executable output is a directory" in result.stderr, (result.stdout, result.stderr) assert not (case / "app0").exists() and not (target / "app0").exists() assert target.exists() == existing @@ -273,7 +273,7 @@ def check_desktop_inputs(relinker, work): registry_path.write_bytes(previous_registry) options = ["--registry"] if registry else [] result = subprocess.run([str(relinker), *options, str(source), str(output)], - capture_output=True, text=True, timeout=20) + capture_output=True, text=True, encoding="utf-8", timeout=20) kind = "Desktop entry" if desktop else "Icon" owner = "an input module" if bundled else "the input executable" assert result.returncode == 2 and f"{kind} output would overwrite {owner}" in result.stderr, (result.stdout, result.stderr) @@ -332,7 +332,7 @@ def main(): arguments = [] if bundled else ["--skip-sce-module"] result = subprocess.run([str(relinker), *options, *arguments, "--registry", str(source), str(output)], - capture_output=True, text=True, timeout=20) + capture_output=True, text=True, encoding="utf-8", timeout=20) expected = "Executable output would overwrite an input module" if bundled else "Executable output would overwrite the input executable" if (result.returncode != 2 or expected not in result.stderr or source.read_bytes() != original @@ -365,7 +365,7 @@ def main(): if existing: output.write_bytes(b"previous output") result = subprocess.run([str(relinker), *options, "--registry", str(source), str(output)], - capture_output=True, text=True, timeout=20) + capture_output=True, text=True, encoding="utf-8", timeout=20) converted = case / "app0" / "sce_module" / "sample.prx.guest.prx" magic = b"MZ" if windows else b"\x7fELF" if (result.returncode != 0 or not output.read_bytes().startswith(magic) diff --git a/core/relinker/relinker/tests/test_tls_diagnostics.py b/core/relinker/relinker/tests/test_tls_diagnostics.py index 51c9614bba..16a566ab08 100644 --- a/core/relinker/relinker/tests/test_tls_diagnostics.py +++ b/core/relinker/relinker/tests/test_tls_diagnostics.py @@ -21,7 +21,7 @@ def main(): def check(input_path, offset, options): result = subprocess.run([str(relinker), "--windows", *options, str(source), str(output)], - capture_output=True, text=True, timeout=30) + capture_output=True, text=True, encoding="utf-8", timeout=30) assert result.returncode == 2, result assert "Unsupported Windows guest TLS instruction" in result.stderr, result.stderr assert f"(bytes: {TLS_INSTRUCTION.hex(' ')})" in result.stderr, result.stderr From 8e31a8526eb378fae03f0b3eff0df5955021edaa Mon Sep 17 00:00:00 2001 From: harisibrar1 <207127467+harisibrar1@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:25:50 -0400 Subject: [PATCH 087/428] test(recompiler): shard shader_cache_runtime_sources to stay under 30 s The test configures a CMake fixture several times and ran as one CTest case: 78 s on a Windows 11 machine, and the Windows CI job warns that it took 66.9 s against the 30 s limit in CONTRIBUTING.md. Its slowest method alone took 27 s because it edited six sources in turn. Split that method into one test per source and register the file as seven CTest shards (--shard INDEX/COUNT, as the conventions_function_bodies tests do). The first shard keeps the shader_cache_runtime_sources name. A self-test checks that every shard count runs each case exactly once. Co-Authored-By: Claude Opus 5.5 --- CMakeLists.txt | 15 +++- .../test_shader_cache_runtime_sources.py | 74 ++++++++++++++++--- 2 files changed, 76 insertions(+), 13 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9eaa66c8db..fd8352951f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -132,10 +132,17 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/test_shader_cache_native_paths.py ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ShaderCacheVersion.cmake ${CMAKE_COMMAND}) set_tests_properties(shader_cache_native_paths PROPERTIES TIMEOUT 20) - add_test(NAME shader_cache_runtime_sources COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_shader_cache_runtime_sources.py - ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler ${CMAKE_COMMAND}) - set_tests_properties(shader_cache_runtime_sources PROPERTIES TIMEOUT 180) + foreach(shard RANGE 0 6) + if(shard EQUAL 0) + set(shader_cache_sources_test_name shader_cache_runtime_sources) + else() + set(shader_cache_sources_test_name shader_cache_runtime_sources_shard_${shard}) + endif() + add_test(NAME ${shader_cache_sources_test_name} COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_shader_cache_runtime_sources.py + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler ${CMAKE_COMMAND} --shard ${shard}/7) + set_tests_properties(${shader_cache_sources_test_name} PROPERTIES TIMEOUT 180) + endforeach() add_test(NAME pr_overlap_encoding COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_encoding.py) add_test(NAME pr_overlap_dependencies COMMAND ${Python3_EXECUTABLE} diff --git a/tools/tests/test_shader_cache_runtime_sources.py b/tools/tests/test_shader_cache_runtime_sources.py index d4d43946e7..dea131cd16 100644 --- a/tools/tests/test_shader_cache_runtime_sources.py +++ b/tools/tests/test_shader_cache_runtime_sources.py @@ -173,17 +173,33 @@ class ShaderCacheRuntimeSourceTests(unittest.TestCase): fixture.configure() self.assertEqual(fixture.version(), previous) - def test_runtime_sources_headers_and_cmake_change_version(self): + def assertEditChangesVersion(self, name): fixture = self.fixture() previous = fixture.version() - for name in ('Recompiler.cpp', 'tests/DummyShaders.cpp', 'tests/DummyShaders.hpp', 'BdaAbi.hpp', 'CMakeLists.txt', 'ShaderCacheVersion.cmake'): - with self.subTest(source=name): - path = fixture.recompiler / name - before = path.read_bytes() - path.write_bytes(before + b'\n') - self.assertNotEqual(fixture.version(), previous) - path.write_bytes(before) - self.assertEqual(fixture.version(), previous) + path = fixture.recompiler / name + before = path.read_bytes() + path.write_bytes(before + b'\n') + self.assertNotEqual(fixture.version(), previous) + path.write_bytes(before) + self.assertEqual(fixture.version(), previous) + + def test_runtime_source_changes_version(self): + self.assertEditChangesVersion('Recompiler.cpp') + + def test_compiled_test_source_changes_version(self): + self.assertEditChangesVersion('tests/DummyShaders.cpp') + + def test_compiled_test_header_changes_version(self): + self.assertEditChangesVersion('tests/DummyShaders.hpp') + + def test_runtime_header_changes_version(self): + self.assertEditChangesVersion('BdaAbi.hpp') + + def test_cmake_list_changes_version(self): + self.assertEditChangesVersion('CMakeLists.txt') + + def test_version_script_changes_version(self): + self.assertEditChangesVersion('ShaderCacheVersion.cmake') def test_flags_change_version(self): fixture = self.fixture() @@ -219,5 +235,45 @@ class ShaderCacheRuntimeSourceTests(unittest.TestCase): self.assertIn('has not been reviewed', fixture.version(False)) +def iter_test_cases(suite): + for test in suite: + if isinstance(test, unittest.TestSuite): + yield from iter_test_cases(test) + else: + yield test + + +def shard_suite(index, count): + if count <= 0 or index < 0 or index >= count: + raise ValueError('shard index must be within the shard count') + tests = sorted(iter_test_cases(unittest.defaultTestLoader.loadTestsFromModule(sys.modules[__name__])), key=lambda test: test.id()) + return unittest.TestSuite(test for position, test in enumerate(tests) if position % count == index) + + +class ShardTests(unittest.TestCase): + def test_shards_run_each_case_once(self): + discovered = sorted(test.id() for test in iter_test_cases(unittest.defaultTestLoader.loadTestsFromModule(sys.modules[__name__]))) + for count in range(1, len(discovered) + 2): + with self.subTest(count=count): + sharded = [test.id() for index in range(count) for test in iter_test_cases(shard_suite(index, count))] + self.assertEqual(sorted(sharded), discovered) + + def test_invalid_shard_is_rejected(self): + for index, count in ((0, 0), (-1, 2), (2, 2)): + with self.subTest(index=index, count=count), self.assertRaises(ValueError): + shard_suite(index, count) + + if __name__ == '__main__': + if len(sys.argv) > 1 and sys.argv[1] == '--shard': + if len(sys.argv) != 3 or '/' not in sys.argv[2]: + print('usage: test_shader_cache_runtime_sources.py [SOURCE CMAKE] --shard INDEX/COUNT', file=sys.stderr) + raise SystemExit(2) + try: + shard_index, shard_count = (int(part) for part in sys.argv[2].split('/', 1)) + suite = shard_suite(shard_index, shard_count) + except ValueError as error: + print(f'invalid shard: {error}', file=sys.stderr) + raise SystemExit(2) + raise SystemExit(0 if unittest.TextTestRunner().run(suite).wasSuccessful() else 1) unittest.main() From df70917cccf4ee2ad88eac1edde0d1215e2ec45d Mon Sep 17 00:00:00 2001 From: hypermist Date: Fri, 23 Oct 2026 01:48:08 +1300 Subject: [PATCH 088/428] fix(agc): ask the swapchain for one image beyond the surface minimum On Wayland with the NVIDIA driver the surface reports minImageCount 2. The presenter acquires with a zero timeout, so once both images were queued no image was ever free again: PPSA29340 flipped at 60 per second but presented 0 frames per 10 s (599 dropped: no free swapchain image) and its window stayed black. Ask for minImageCount + 1, capped by maxImageCount when the surface has one. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- .../Execution/include/SwapchainImages.hpp | 17 +++++++++++++++++ .../Execution/src/VulkanDevice.cpp | 5 +++-- core/libs/prx/libSceAgcDriver/tests/Flip.cpp | 12 ++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/SwapchainImages.hpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/SwapchainImages.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/SwapchainImages.hpp new file mode 100644 index 0000000000..032b5fc86c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/SwapchainImages.hpp @@ -0,0 +1,17 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SWAPCHAINIMAGES_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SWAPCHAINIMAGES_HPP + +#include +#include +#include + +namespace AgcDriver { + +inline std::uint32_t SwapchainImageCount(const VkSurfaceCapabilitiesKHR& surface) { + const std::uint32_t count = surface.minImageCount + 1; + return surface.maxImageCount != 0 ? std::min(count, surface.maxImageCount) : count; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 89ad6e3157..d57c6ad044 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -22,6 +22,7 @@ #include "prx/libSceAgcDriver/Execution/include/SubgroupClock.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentMode.hpp" +#include "prx/libSceAgcDriver/Execution/include/SwapchainImages.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationPass.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" @@ -2069,7 +2070,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : require((surface.supportedUsageFlags & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) != 0, "surface does not support color attachment images"); VkSwapchainCreateInfoKHR swapchain{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR}; swapchain.surface = state->surface; - swapchain.minImageCount = surface.minImageCount; + swapchain.minImageCount = AgcDriver::SwapchainImageCount(surface); swapchain.imageFormat = state->surfaceFormat; swapchain.imageColorSpace = state->surfaceColorSpace; swapchain.imageExtent = state->extent; @@ -2883,7 +2884,7 @@ void VulkanDevice::recreateSwapchain(std::uint32_t width, std::uint32_t height, require((surface.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0 && (surface.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR) != 0 && (surface.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) != 0, "resized surface capabilities are unsupported"); VkSwapchainCreateInfoKHR create{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR}; create.surface = state->surface; - create.minImageCount = surface.minImageCount; + create.minImageCount = AgcDriver::SwapchainImageCount(surface); create.imageFormat = state->surfaceFormat; create.imageColorSpace = state->surfaceColorSpace; create.imageExtent = {width, height}; diff --git a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp index ec73dea01a..95c49020b9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentMode.hpp" +#include "prx/libSceAgcDriver/Execution/include/SwapchainImages.hpp" #include "prx/libc/include/Shutdown.hpp" #include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" @@ -402,6 +403,17 @@ void testReset(bool compute) { void testPresentMode() { using AgcDriver::SelectPresentMode; + VkSurfaceCapabilitiesKHR surface{}; + surface.minImageCount = 2; + surface.maxImageCount = 8; + check(AgcDriver::SwapchainImageCount(surface) == 3, "the swapchain has no image beyond the surface minimum"); + surface.minImageCount = 3; + check(AgcDriver::SwapchainImageCount(surface) == 4, "the swapchain does not follow a larger surface minimum"); + surface.maxImageCount = 3; + check(AgcDriver::SwapchainImageCount(surface) == 3, "the swapchain exceeds the surface maximum"); + surface.minImageCount = 2; + surface.maxImageCount = 0; + check(AgcDriver::SwapchainImageCount(surface) == 3, "an unlimited surface maximum capped the swapchain"); const std::array wayland{VK_PRESENT_MODE_FIFO_KHR, VK_PRESENT_MODE_IMMEDIATE_KHR}; const std::array all{VK_PRESENT_MODE_IMMEDIATE_KHR, VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_FIFO_KHR}; check(SelectPresentMode(nullptr, wayland) == VK_PRESENT_MODE_FIFO_KHR, "the default present mode is not FIFO"); From 45f4739c8d6263934f7439eca7dbe403ba871efa Mon Sep 17 00:00:00 2001 From: Rookie <175737347+rooklz@users.noreply.github.com> Date: Thu, 22 Oct 2026 05:39:42 +0100 Subject: [PATCH 089/428] fix(libc): build getifaddrs netmasks from the address family on macOS hosts --- core/libs/prx/libc/src/Interfaces.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Interfaces.cpp b/core/libs/prx/libc/src/Interfaces.cpp index 8e39cfa29b..af71f5f9e9 100644 --- a/core/libs/prx/libc/src/Interfaces.cpp +++ b/core/libs/prx/libc/src/Interfaces.cpp @@ -88,6 +88,22 @@ std::vector GuestAddress(const sockaddr* address) { return GuestIpv6(reinterpret_cast(&v6->sin6_addr), v6->sin6_scope_id); } +#ifndef _WIN32 +std::vector GuestNetmask(const sockaddr* netmask, int family) { + sockaddr_storage mask{}; + const std::size_t size = family == AF_INET ? sizeof(sockaddr_in) : sizeof(sockaddr_in6); +#ifdef __APPLE__ + const std::size_t length = std::min(netmask->sa_len, size); +#else + const std::size_t length = size; +#endif + if (length > offsetof(sockaddr, sa_data)) + std::memcpy(reinterpret_cast(&mask) + offsetof(sockaddr, sa_data), netmask->sa_data, length - offsetof(sockaddr, sa_data)); + mask.ss_family = static_cast(family); + return GuestAddress(reinterpret_cast(&mask)); +} +#endif + std::string CheckedName(const char* name) { const std::string checked(name); if (checked.empty() || checked.size() >= guestNameCapacity) @@ -304,7 +320,7 @@ int HostEntries(std::vector& entries) { } if (item->ifa_addr->sa_family != AF_INET && item->ifa_addr->sa_family != AF_INET6) continue; if (!item->ifa_netmask) throw std::runtime_error(std::string("getifaddrs: host address without a netmask on ") + item->ifa_name); - Entry entry{CheckedName(item->ifa_name), InterfaceFlags(item->ifa_flags), GuestAddress(item->ifa_addr), GuestAddress(item->ifa_netmask), {}}; + Entry entry{CheckedName(item->ifa_name), InterfaceFlags(item->ifa_flags), GuestAddress(item->ifa_addr), GuestNetmask(item->ifa_netmask, item->ifa_addr->sa_family), {}}; const int family = item->ifa_addr->sa_family; if (item->ifa_flags & IFF_POINTOPOINT) { if (item->ifa_dstaddr && item->ifa_dstaddr->sa_family == family) entry.destination = GuestAddress(item->ifa_dstaddr); From 7c213787ac017ad477eea3e1ac5414767e671549 Mon Sep 17 00:00:00 2001 From: Rookie <175737347+rooklz@users.noreply.github.com> Date: Thu, 22 Oct 2026 05:51:49 +0100 Subject: [PATCH 090/428] fix(libkernel): treat a socket direction that is already shut as shut on macOS hosts --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 3b412c4a5a..7642377219 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -1290,11 +1290,30 @@ int APS5_VABI bind_nid_postfix(int descriptor, const void* address, std::uint32_ if (!Address(address, length, native, size)) return -1; return ::bind(socket->value, reinterpret_cast(&native), size) ? Fail(NativeError()) : 0; } +namespace { +bool AlreadyShut(NativeSocket socket, int how) { +#ifdef __APPLE__ + if (errno != ENOTCONN) return false; + sockaddr_storage peer{}; + socklen_t size = sizeof(peer); + if (::getpeername(socket, reinterpret_cast(&peer), &size)) { + errno = ENOTCONN; + return false; + } + return how != SHUT_RDWR || ::shutdown(socket, SHUT_WR) == 0 || errno == ENOTCONN; +#else + (void)socket; + (void)how; + return false; +#endif +} +} + int APS5_VABI shutdown_nid_postfix(int descriptor, int how) { const auto socket = Lookup(descriptor); if (!socket) return -1; if (how < 0 || how > 2) return Fail(22); - if (::shutdown(socket->value, how)) return Fail(NativeError()); + if (::shutdown(socket->value, how) && !AlreadyShut(socket->value, how)) return Fail(NativeError()); std::lock_guard lock(socket->modeMutex); if (how != 1) socket->receiveShut = true; if (how != 0) socket->sendShut = true; From 035eddac1a27f1607e727c52b43ac9bc90e46634 Mon Sep 17 00:00:00 2001 From: Rookie <175737347+rooklz@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:02:41 +0100 Subject: [PATCH 091/428] test(libSceNet): wait for the blocked send to start before aborting it --- core/libs/tests/NetBlockingSend.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/tests/NetBlockingSend.cpp b/core/libs/tests/NetBlockingSend.cpp index 466cbc279a..6dba4d4900 100644 --- a/core/libs/tests/NetBlockingSend.cpp +++ b/core/libs/tests/NetBlockingSend.cpp @@ -130,6 +130,14 @@ int main(int argc, char** argv) { { Connection connection; auto sending = std::async(std::launch::async, [&] { return Send(api, connection.sender, bytes); }); + const auto entered = std::chrono::steady_clock::now() + std::chrono::seconds(5); + char peeked = 0; + bool arrived = false; + while (!arrived && std::chrono::steady_clock::now() < entered) { + arrived = sceNetRecv(connection.receiver, &peeked, 1, NET_MSG_PEEK | NET_MSG_DONTWAIT) > 0; + if (!arrived) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + Require(arrived, "send did not reach the receiver before abort"); Require(sending.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout, "send did not wait before abort"); Require(sceNetSocketAbort(connection.sender, 0) == 0, "abort a blocked send"); Require(sending.wait_for(std::chrono::seconds(2)) == std::future_status::ready, "abort did not wake a blocked send"); From 4892b714948c4af3b38e9ac984fb7adfb569cbca Mon Sep 17 00:00:00 2001 From: Dom3005 Date: Wed, 21 Oct 2026 18:14:09 +0200 Subject: [PATCH 092/428] docs(debt): match the Windows directory descriptor entry to the code d31e28308 made getdents and getdirentries throw on a name above the one-byte d_namlen limit, and #8591 made getdirentries report the entry index of its block through basep. The libkernel entry still described ENAMETOOLONG and a basep of 0. Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 72a0e537c4..6e4fc37006 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -135,7 +135,7 @@ - On Linux, [direct memory](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again. - [system](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified - [sceKernelWriteThrottlingStatus](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed; returns `EFAULT` (`0x8002000e`) when passed a null status buffer pointer. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown -- [Windows directory descriptors](../../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. Names whose UTF-8 encoding exceeds the one-byte `d_namlen` limit are not representable in this guest directory-record layout: `getdents` returns `ENAMETOOLONG` without consuming the entry. Repeated calls therefore report the same error and entries after that name cannot be reached through this descriptor; FreeBSD's `getdents` behavior for host filenames that exceed its `MAXNAMLEN` is not verified. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies. Descriptors retain the opened directory's native handle across renames. Enumeration reloads and `*at` operations resolve its current path, so a concurrent rename between resolution and the operation is not atomic. Enumeration of a removed directory and network-share path queries have not been verified +- [Windows directory descriptors](../../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. Names whose UTF-8 encoding exceeds the one-byte `d_namlen` limit are not representable in this guest directory-record layout, so `getdents` and `getdirentries` throw when they reach one; FreeBSD's behavior for host filenames that exceed its `MAXNAMLEN` is not verified. `getdirentries` reports through `basep` the entry index its block starts at, so only 0 and the values from `basep` or `lseek` are valid offsets; FreeBSD offsets are filesystem cookies. Descriptors retain the opened directory's native handle across renames. Enumeration reloads and `*at` operations resolve its current path, so a concurrent rename between resolution and the operation is not atomic. Enumeration of a removed directory and network-share path queries have not been verified - [Linux directory entry types](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) - `sceKernelGetdirentries` reports a host symlink inside the app0 tree as the type of its target (directory or regular file), since a console app0 has no links and links there come from how the title was unpacked; a link elsewhere, or a dangling one, stays a link. A directory opened through an app0 link resolves to its target's host path, so links nested inside that target keep the link type. - [ftruncate, truncate, sceKernelFtruncate](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) extend a file on Windows with `SetFileInformationByHandle` (`FileEndOfFileInfo`), which reserves the clusters but writes no zero bytes, and report `EFBIG` for a length the volume cannot hold (`ERROR_INVALID_PARAMETER` on a disk file; seen with a length of `INT64_MAX`, the size from which NTFS refuses it was not measured) and `ENOSPC` when the free space is smaller. FreeBSD 11.4 makes the extension a hole on a file system that has them ([`ffs_truncate`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/ufs/ffs/ffs_inode.c) writes only the last block), so a length above the free space succeeds there. A write behind the end of the file still makes NTFS write zero bytes over the gap, as the file is not sparse. None of this was measured on a console. - [readv, writev, preadv, pwritev and their sceKernel forms](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) - unreadable iovec-table errors follow FreeBSD 11.4's [`copyinuio`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/subr_uio.c#L401-L434), not console measurements. The table is copied before inspecting its entries; payload-buffer validation is unchanged. A Linux `process_vm_readv` failure other than `EFAULT` throws, including when the host denies that API. From 3a08b7ce40a4de1ddc88e68fc99dc91553af0dc6 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 22 Oct 2026 14:53:01 +0100 Subject: [PATCH 093/428] test(libc): cover the sized find, count, min and max vector helpers guest_vector_algorithms ran one size of each MSVC-STL vector helper. It now also runs __std_find_trivial 1, 4 and 8 (first match, or last when there is none), __std_count_trivial 1, 2 and 4, __std_min_element 2, 4 and 8 and __std_max_element 1, 2 and 8. The min and max checks cover signed against unsigned ordering, a tie (the first extreme is returned) and an empty range. --- core/libs/tests/GuestVectorAlgorithms.cpp | 50 +++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/core/libs/tests/GuestVectorAlgorithms.cpp b/core/libs/tests/GuestVectorAlgorithms.cpp index c3b858bac6..c40f008d86 100644 --- a/core/libs/tests/GuestVectorAlgorithms.cpp +++ b/core/libs/tests/GuestVectorAlgorithms.cpp @@ -4,6 +4,21 @@ #include #include +extern "C" { +const void* APS5_VABI __std_find_trivial_1_nid_postfix(const void*, const void*, std::uint8_t); +const void* APS5_VABI __std_find_trivial_4_nid_postfix(const void*, const void*, std::uint32_t); +const void* APS5_VABI __std_find_trivial_8_nid_postfix(const void*, const void*, std::uint64_t); +std::size_t APS5_VABI __std_count_trivial_1_nid_postfix(const void*, const void*, std::uint8_t); +std::size_t APS5_VABI __std_count_trivial_2_nid_postfix(const void*, const void*, std::uint16_t); +std::size_t APS5_VABI __std_count_trivial_4_nid_postfix(const void*, const void*, std::uint32_t); +const void* APS5_VABI __std_min_element_2_nid_postfix(const void*, const void*, bool); +const void* APS5_VABI __std_min_element_4_nid_postfix(const void*, const void*, bool); +const void* APS5_VABI __std_min_element_8_nid_postfix(const void*, const void*, bool); +const void* APS5_VABI __std_max_element_1_nid_postfix(const void*, const void*, bool); +const void* APS5_VABI __std_max_element_2_nid_postfix(const void*, const void*, bool); +const void* APS5_VABI __std_max_element_8_nid_postfix(const void*, const void*, bool); +} + struct MinMaxPointers { const void* minimum; const void* maximum; @@ -25,6 +40,40 @@ void APS5_VABI __std_swap_ranges_trivially_swappable_noalias_nid_postfix(void*, static void Require(bool value) { if (!value) std::abort(); } +static void CheckSizedHelpers() { + const std::uint8_t bytes[] = {3, 0xff, 3, 9, 0xff}; + Require(__std_find_trivial_1_nid_postfix(bytes, bytes + 5, 0xff) == bytes + 1); + Require(__std_find_trivial_1_nid_postfix(bytes, bytes + 5, 4) == bytes + 5); + Require(__std_find_trivial_1_nid_postfix(bytes, bytes, 3) == bytes); + Require(__std_count_trivial_1_nid_postfix(bytes, bytes + 5, 3) == 2 && __std_count_trivial_1_nid_postfix(bytes, bytes + 5, 7) == 0); + Require(__std_max_element_1_nid_postfix(bytes, bytes + 5, false) == bytes + 1); + Require(__std_max_element_1_nid_postfix(bytes, bytes + 5, true) == bytes + 3); + + const std::uint16_t halves[] = {0x8000, 5, 0x7fff, 5, 0x8000}; + Require(__std_count_trivial_2_nid_postfix(halves, halves + 5, 5) == 2 && __std_count_trivial_2_nid_postfix(halves, halves, 5) == 0); + Require(__std_min_element_2_nid_postfix(halves, halves + 5, false) == halves + 1); + Require(__std_min_element_2_nid_postfix(halves, halves + 5, true) == halves); + Require(__std_max_element_2_nid_postfix(halves, halves + 5, false) == halves); + Require(__std_max_element_2_nid_postfix(halves, halves + 5, true) == halves + 2); + + const std::uint32_t words[] = {7, 0xfffffff0u, 1, 1, 0xfffffff0u}; + Require(__std_find_trivial_4_nid_postfix(words, words + 5, 1) == words + 2); + Require(__std_find_trivial_4_nid_postfix(words, words + 5, 2) == words + 5); + Require(__std_count_trivial_4_nid_postfix(words, words + 5, 0xfffffff0u) == 2); + Require(__std_min_element_4_nid_postfix(words, words + 5, false) == words + 2); + Require(__std_min_element_4_nid_postfix(words, words + 5, true) == words + 1); + Require(__std_min_element_4_nid_postfix(words, words, true) == words); + + const std::uint64_t wides[] = {0x8000000000000000ull, 2, 0x7fffffffffffffffull, 0x8000000000000000ull}; + Require(__std_find_trivial_8_nid_postfix(wides, wides + 4, 0x8000000000000000ull) == wides); + Require(__std_find_trivial_8_nid_postfix(wides + 1, wides + 4, 0x8000000000000000ull) == wides + 3); + Require(__std_min_element_8_nid_postfix(wides, wides + 4, true) == wides); + Require(__std_min_element_8_nid_postfix(wides, wides + 4, false) == wides + 1); + Require(__std_max_element_8_nid_postfix(wides, wides + 4, false) == wides); + Require(__std_max_element_8_nid_postfix(wides, wides + 4, true) == wides + 2); + Require(__std_max_element_8_nid_postfix(wides, wides, false) == wides); +} + static void CheckFindAndCount() { const std::uint16_t halves[] = {5, 7, 9, 7}; Require(__std_find_trivial_2_nid_postfix(halves, halves + 4, 7) == halves + 1); @@ -88,6 +137,7 @@ static void CheckReverseAndSwap() { } int main() { + CheckSizedHelpers(); CheckFindAndCount(); CheckUnsizedRescan(); CheckMinMax(); From 3db9e4ee0577f4184554942a1482e43e4c8e924f Mon Sep 17 00:00:00 2001 From: David Date: Thu, 22 Oct 2026 14:57:02 +0100 Subject: [PATCH 094/428] test(libc): cover the standard string exports guest_strings checked the bounds-checked and BSD string functions but not the plain C ones. It now checks strcmp, strncmp and strcoll by sign, strlen, strcpy and strcat return values, strncpy zero padding and its unterminated truncation, strstr with an empty needle, strspn, bcmp and bzero. --- core/libs/tests/GuestStrings.cpp | 49 ++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index ef2f2f0a69..34eeb0aa03 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -8,6 +8,20 @@ #include #include +extern "C" { +void APS5_VABI bzero_nid_postfix(void* destination, size_t count); +int APS5_VABI bcmp_nid_postfix(const void* s1, const void* s2, size_t n); +int APS5_VABI strcmp_nid_postfix(const char* s1, const char* s2); +int APS5_VABI strncmp_nid_postfix(const char* s1, const char* s2, size_t n); +int APS5_VABI strcoll_nid_postfix(const char* s1, const char* s2); +size_t APS5_VABI strlen_nid_postfix(const char* s); +char* APS5_VABI strcpy_nid_postfix(char* dest, const char* src); +char* APS5_VABI strncpy_nid_postfix(char* dest, const char* src, size_t count); +char* APS5_VABI strcat_nid_postfix(char* dest, const char* src); +char* APS5_VABI strstr_nid_postfix(const char* haystack, const char* needle); +size_t APS5_VABI strspn_nid_postfix(const char* s, const char* accept); +} + extern "C" { void APS5_VABI bcopy_nid_postfix(const void*, void*, std::size_t); char* APS5_VABI basename_nid_postfix(const char*); @@ -82,6 +96,40 @@ static bool CheckMemcpyOverlap() { return correct; } +static int Sign(int value) { + return value < 0 ? -1 : value > 0 ? 1 : 0; +} + +static void CheckStandardStrings() { + Require(strcmp_nid_postfix("abc", "abc") == 0 && Sign(strcmp_nid_postfix("abc", "abd")) == -1 && Sign(strcmp_nid_postfix("abc", "ab")) == 1); + Require(Sign(strcmp_nid_postfix("\x80", "\x7f")) == 1); + Require(strncmp_nid_postfix("abcX", "abcY", 3) == 0 && Sign(strncmp_nid_postfix("abcX", "abcY", 4)) == -1 && strncmp_nid_postfix("a", "b", 0) == 0); + Require(strcoll_nid_postfix("same", "same") == 0 && Sign(strcoll_nid_postfix("B", "a")) == -1); + Require(strlen_nid_postfix("") == 0 && strlen_nid_postfix("seven 7") == 7); + + char buffer[16]; + std::memset(buffer, 'x', sizeof(buffer)); + Require(strcpy_nid_postfix(buffer, "ab") == buffer && std::memcmp(buffer, "ab\0x", 4) == 0); + Require(strcat_nid_postfix(buffer, "cd") == buffer && std::strcmp(buffer, "abcd") == 0); + + std::memset(buffer, 'x', sizeof(buffer)); + Require(strncpy_nid_postfix(buffer, "ab", 5) == buffer && std::memcmp(buffer, "ab\0\0\0x", 6) == 0); + std::memset(buffer, 'x', sizeof(buffer)); + Require(strncpy_nid_postfix(buffer, "abcdef", 3) == buffer && std::memcmp(buffer, "abcx", 4) == 0); + + const char* text = "needle in a haystack"; + Require(strstr_nid_postfix(text, "hay") == text + 12 && strstr_nid_postfix(text, "") == text && strstr_nid_postfix(text, "hey") == nullptr); + Require(strspn_nid_postfix("aabbc", "ab") == 4 && strspn_nid_postfix("xyz", "ab") == 0 && strspn_nid_postfix("ab", "") == 0); + + unsigned char bytes[6] = {1, 2, 3, 4, 5, 6}; + const unsigned char same[6] = {1, 2, 3, 4, 5, 6}; + Require(bcmp_nid_postfix(bytes, same, 6) == 0 && bcmp_nid_postfix(bytes, "zz", 0) == 0); + bytes[5] = 9; + Require(bcmp_nid_postfix(bytes, same, 6) != 0 && bcmp_nid_postfix(bytes, same, 5) == 0); + bzero_nid_postfix(bytes + 1, 4); + Require(bytes[0] == 1 && bytes[1] == 0 && bytes[4] == 0 && bytes[5] == 9); +} + static void CheckSecureCopySizeLimits() { using Copy = int (APS5_VABI*)(void*, std::size_t, const void*, std::size_t); constexpr std::size_t RsizeMax = SIZE_MAX >> 1; @@ -327,6 +375,7 @@ static void CheckCharacterConversionInSearches() { } int main() { + CheckStandardStrings(); CheckStrtonum(); CheckBcopy(); if (!CheckDirnameLengths()) return 1; From a5c2df716fa02bf20bd245feec1f720edb9b90ae Mon Sep 17 00:00:00 2001 From: David Date: Thu, 22 Oct 2026 15:00:50 +0100 Subject: [PATCH 095/428] test(libc): cover the exact long double and float rounding exports guest_math_exact did not reach copysignl, sqrtl, lrintl, llrintl, lrintf, llrintf, scalbf, scalblnf or __isinfl. It now checks copysignl with signed zeros, sqrtl at -1 and -0, the four lrint forms at +-2.5 under each rounding mode with errno untouched, scalbf with fractional, infinite and NaN exponents, scalblnf clamping 64-bit exponents, and __isinfl. --- core/libs/tests/GuestMathExact.cpp | 47 ++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/core/libs/tests/GuestMathExact.cpp b/core/libs/tests/GuestMathExact.cpp index e601e88973..d4b4f97a9b 100644 --- a/core/libs/tests/GuestMathExact.cpp +++ b/core/libs/tests/GuestMathExact.cpp @@ -7,6 +7,18 @@ #include #include +extern "C" { +long double APS5_VABI copysignl_nid_postfix(long double x, long double y); +long double APS5_VABI sqrtl_nid_postfix(long double x); +std::int64_t APS5_VABI lrintl_nid_postfix(long double x); +long long APS5_VABI llrintl_nid_postfix(long double x); +std::int64_t APS5_VABI lrintf_nid_postfix(float x); +long long APS5_VABI llrintf_nid_postfix(float x); +float APS5_VABI scalblnf_nid_postfix(float x, std::int64_t exponent); +float APS5_VABI scalbf_nid_postfix(float x, float exponent); +int APS5_VABI __isinfl_nid_postfix(long double x); +} + extern "C" { long double APS5_VABI fdiml_nid_postfix(long double, long double); long double APS5_VABI fmodl_nid_postfix(long double, long double); @@ -49,6 +61,40 @@ static long double Extended(std::uint16_t signExponent, std::uint64_t mantissa) static bool SameBits(long double left, long double right) { return std::memcmp(&left, &right, 10) == 0; } +static void CheckRoundingAndScaling() { + Require(copysignl_nid_postfix(3.0L, -1.0L) == -3.0L && copysignl_nid_postfix(-3.0L, 0.0L) == 3.0L); + Require(std::signbit(copysignl_nid_postfix(0.0L, -0.0L)) && !std::signbit(copysignl_nid_postfix(-0.0L, 2.0L))); + Require(sqrtl_nid_postfix(6.25L) == 2.5L && std::isnan(sqrtl_nid_postfix(-1.0L))); + Require(sqrtl_nid_postfix(-0.0L) == 0.0L && std::signbit(sqrtl_nid_postfix(-0.0L))); + + const int saved = std::fegetround(); + const struct { + int mode; + long long half; + long long negativeHalf; + } modes[] = {{FE_TONEAREST, 2, -2}, {FE_UPWARD, 3, -2}, {FE_DOWNWARD, 2, -3}, {FE_TOWARDZERO, 2, -2}}; + for (const auto& mode : modes) { + Require(std::fesetround(mode.mode) == 0); + errno = 0; + Require(lrintl_nid_postfix(2.5L) == mode.half && lrintl_nid_postfix(-2.5L) == mode.negativeHalf); + Require(llrintl_nid_postfix(2.5L) == mode.half && llrintl_nid_postfix(-2.5L) == mode.negativeHalf); + Require(lrintf_nid_postfix(2.5f) == mode.half && lrintf_nid_postfix(-2.5f) == mode.negativeHalf); + Require(llrintf_nid_postfix(2.5f) == mode.half && llrintf_nid_postfix(-2.5f) == mode.negativeHalf); + Require(errno == 0); + } + std::fesetround(saved); + Require(llrintl_nid_postfix(3.5L) == 4 && lrintf_nid_postfix(16777216.0f) == 16777216); + + Require(scalbf_nid_postfix(1.5f, 3.0f) == 12.0f && scalbf_nid_postfix(12.0f, -2.0f) == 3.0f); + Require(std::isnan(scalbf_nid_postfix(1.0f, 0.5f)) && std::isnan(scalbf_nid_postfix(NAN, 1.0f))); + Require(std::isinf(scalbf_nid_postfix(1.0f, INFINITY)) && scalbf_nid_postfix(8.0f, -INFINITY) == 0.0f); + Require(scalblnf_nid_postfix(1.0f, 10) == 1024.0f && scalblnf_nid_postfix(1024.0f, -10) == 1.0f); + Require(std::isinf(scalblnf_nid_postfix(1.0f, INT64_MAX)) && scalblnf_nid_postfix(1.0f, INT64_MIN) == 0.0f); + + Require(__isinfl_nid_postfix(static_cast(INFINITY)) == 1 && __isinfl_nid_postfix(-static_cast(INFINITY)) == 1); + Require(__isinfl_nid_postfix(1.0L) == 0 && __isinfl_nid_postfix(static_cast(NAN)) == 0 && __isinfl_nid_postfix(0.0L) == 0); +} + static void CheckSignalingNanPassThrough() { const long double signaling = Extended(0x7fff, 0x8000000000001234ull); Require(SameBits(fdiml_nid_postfix(signaling, 1.0L), signaling)); @@ -133,6 +179,7 @@ static void CheckClassification() { } int main() { + CheckRoundingAndScaling(); CheckSignalingNanPassThrough(); CheckErrno(); CheckConsoleQuirks(); From 68e3a685fb6adce8ee16d14af4550fc09859601f Mon Sep 17 00:00:00 2001 From: hypermist Date: Fri, 23 Oct 2026 03:03:15 +1300 Subject: [PATCH 096/428] fix(libSceSpSysCallWrapper): reach sceNetRecv and sceNetSend through longer-named entries The PE and Mach-O NID patchers rewrite each sce import name in place with its 11-character NID, so the 10-character sceNetRecv and sceNetSend imports stopped the Windows build. libSceNet now exports GuestNetRecv_nid_no_patch and GuestNetSend_nid_no_patch, as libkernel does with GuestAlarm_nid_no_patch, and the two wrappers call those. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SNqSvnFwRBz3D76n476yw6 --- core/libs/prx/libSceNet/Export.cpp | 8 ++++++++ core/libs/prx/libSceSpSysCallWrapper/Export.cpp | 8 ++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 4861eac26d..d23001992c 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -1265,6 +1265,10 @@ int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { return result >= 0 ? result : fail(receive_error(socket)); } +int64_t APS5_VABI GuestNetRecv_nid_no_patch(int s, void* buf, size_t len, int flags) { + return sceNetRecv(s, buf, len, flags); +} + int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* from, uint32_t* fromlen) { if (!message_flags_supported(flags, NET_MSG_PEEK | NET_MSG_DONTWAIT)) return fail(NET_EOPNOTSUPP); if (!buf && len) return fail(NET_EINVAL); @@ -1325,6 +1329,10 @@ int64_t APS5_VABI sceNetSend(int s, const void* buf, size_t len, int flags) { return result >= 0 ? result : fail(native_error()); } +int64_t APS5_VABI GuestNetSend_nid_no_patch(int s, const void* buf, size_t len, int flags) { + return sceNetSend(s, buf, len, flags); +} + int64_t APS5_VABI sceNetSendto(int s, const void* buf, size_t len, int flags, const void* to, uint32_t tolen) { if (!message_flags_supported(flags, NET_MSG_DONTWAIT)) return fail(NET_EOPNOTSUPP); if (!buf && len) return fail(NET_EINVAL); diff --git a/core/libs/prx/libSceSpSysCallWrapper/Export.cpp b/core/libs/prx/libSceSpSysCallWrapper/Export.cpp index deb4127d26..e5883ecdd1 100644 --- a/core/libs/prx/libSceSpSysCallWrapper/Export.cpp +++ b/core/libs/prx/libSceSpSysCallWrapper/Export.cpp @@ -5,6 +5,8 @@ extern "C" { +std::int64_t APS5_VABI GuestNetRecv_nid_no_patch(int s, void* buf, std::size_t len, int flags); +std::int64_t APS5_VABI GuestNetSend_nid_no_patch(int s, const void* buf, std::size_t len, int flags); int APS5_VABI sceKernelGettimeofday(KernelTimeval* tp); int APS5_VABI sceKernelNanosleep(const KernelTimespec* rqtp, KernelTimespec* rmtp); int APS5_VABI sceNetConnect(int s, const void* addr, std::uint32_t addrlen); @@ -16,13 +18,11 @@ int APS5_VABI sceNetEpollWait(int eid, void* events, int maxevents, int timeout) int* APS5_VABI sceNetErrnoLoc(void); int APS5_VABI sceNetPoolCreate(const char* name, int size, int flags); int APS5_VABI sceNetPoolDestroy(int memid); -std::int64_t APS5_VABI sceNetRecv(int s, void* buf, std::size_t len, int flags); int APS5_VABI sceNetResolverAbort(int rid, int flags); int APS5_VABI sceNetResolverCreate(const char* name, int memid, int flags); int APS5_VABI sceNetResolverDestroy(int rid); int APS5_VABI sceNetResolverGetError(int rid, int* status); int APS5_VABI sceNetResolverStartNtoa(int rid, const char* hostname, void* addr, int timeout, int retry, int flags); -std::int64_t APS5_VABI sceNetSend(int s, const void* buf, std::size_t len, int flags); int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval, std::uint32_t optlen); int APS5_VABI sceNetSocket(const char* name, int family, int type, int protocol); int APS5_VABI sceNetSocketClose(int s); @@ -101,7 +101,7 @@ int APS5_VABI sceSpNetPoolDestroy(int memid) { } std::int64_t APS5_VABI sceSpNetRecv(int s, void* buf, std::size_t len, int flags) { - return sceNetRecv(s, buf, len, flags); + return GuestNetRecv_nid_no_patch(s, buf, len, flags); } int APS5_VABI sceSpNetResolverAbort(int rid, int flags) { @@ -125,7 +125,7 @@ int APS5_VABI sceSpNetResolverStartNtoa(int rid, const char* hostname, void* add } std::int64_t APS5_VABI sceSpNetSend(int s, const void* buf, std::size_t len, int flags) { - return sceNetSend(s, buf, len, flags); + return GuestNetSend_nid_no_patch(s, buf, len, flags); } int APS5_VABI sceSpNetSetsockopt(int s, int level, int optname, const void* optval, std::uint32_t optlen) { From a30c574aeaaa16cc255b323971db09d9a1a4bea0 Mon Sep 17 00:00:00 2001 From: Shahul Mubeen A <174251444+shahulmubeen-a@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:56:56 +0530 Subject: [PATCH 097/428] fix(libSceAgc): accept register lists at any byte address sceAgcCbSetShRegistersDirect and sceAgcCbSetUcRegistersDirect refused a register list that was not 4-byte aligned. PPSA26344 passes the sh register list of a shader header that sits 1 byte past an 8-byte boundary (core_common_subcore.core_b1_fx.sprig.xpps; the unaligned headers themselves are accepted since #902), and the title aborted with "null or misaligned address 0x8021363131" after about 11 minutes in the open world. The list entries are now copied byte for byte, as #7606 does for register range values. A null list with a count still throws. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libSceAgc/Command/src/Packet.cpp | 14 ++++++++---- .../prx/libSceAgc/tests/RegisterCommands.cpp | 22 +++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgc/Command/src/Packet.cpp b/core/libs/prx/libSceAgc/Command/src/Packet.cpp index 6ec0e12165..c96ff5165e 100644 --- a/core/libs/prx/libSceAgc/Command/src/Packet.cpp +++ b/core/libs/prx/libSceAgc/Command/src/Packet.cpp @@ -113,17 +113,23 @@ std::uint32_t* WriteRegisters(CommandBuffer* buffer, std::uint32_t opcode, const Require(buffer != nullptr, function, "null command buffer"); return buffer->cursor_up; } - CheckAddress(reinterpret_cast(registers), 4, function); + CheckAddress(reinterpret_cast(registers), 1, function); + const auto* bytes = reinterpret_cast(const_cast(registers)); + const auto entryAt = [&](std::uint32_t index) { + ShaderRegister entry; + std::memcpy(&entry, bytes + static_cast(index) * sizeof(ShaderRegister), sizeof(entry)); + return entry; + }; std::vector snapshot; if (snapshotAll) { snapshot.reserve(count); for (std::uint32_t i = 0; i < count; ++i) { - snapshot.push_back({registers[i].offset, registers[i].value}); + snapshot.push_back(entryAt(i)); CheckBits(snapshot.back().offset, 0xffffu, function); } } - const auto offsetAt = [&](std::uint32_t index) { return snapshotAll ? snapshot[index].offset : registers[index].offset; }; - const auto valueAt = [&](std::uint32_t index) { return snapshotAll ? snapshot[index].value : registers[index].value; }; + const auto offsetAt = [&](std::uint32_t index) { return snapshotAll ? snapshot[index].offset : entryAt(index).offset; }; + const auto valueAt = [&](std::uint32_t index) { return snapshotAll ? snapshot[index].value : entryAt(index).value; }; std::uint32_t* first = nullptr; std::uint32_t index = 0; while (index < count) { diff --git a/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp b/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp index 5e6be0c709..2c8c7c4753 100644 --- a/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp +++ b/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include extern "C" { @@ -125,6 +126,26 @@ void testRangeFromAnyByte() { } } +void testListFromAnyByte() { + const std::array writers{sceAgcCbSetShRegistersDirect, sceAgcCbSetUcRegistersDirect}; + const std::array opcodes{0x7600u, 0x7900u}; + const std::array registers{{{0x10u, 0x11223344u}, {0x11u, 0x55667788u}, {0x30u, 0x99aabbccu}}}; + for (std::size_t i = 0; i < writers.size(); ++i) { + for (const std::size_t offset : {1u, 2u, 3u, 5u}) { + alignas(16) std::array bytes{}; + std::memcpy(bytes.data() + offset, registers.data(), sizeof(registers)); + Storage storage; + storage.words.fill(0xabcdef01u); + const auto* list = reinterpret_cast(bytes.data() + offset); + auto* packet = writers[i](&storage.buffer, list, static_cast(registers.size())); + const std::array expected{0xc0020000u | opcodes[i], 0x10u, 0x11223344u, 0x55667788u, 0xc0010000u | opcodes[i], 0x30u, 0x99aabbccu}; + check(packet == storage.words.data() && storage.buffer.cursor_up == packet + expected.size(), "unaligned register list allocation mismatch"); + check(std::equal(expected.begin(), expected.end(), packet), "unaligned register list was not read byte for byte"); + check(packet[expected.size()] == 0xabcdef01u, "unaligned register list overwrote the following word"); + } + } +} + void testIndirect() { const std::array writers{sceAgcDcbSetCxRegistersIndirect, sceAgcDcbSetShRegistersIndirect, sceAgcDcbSetUcRegistersIndirect}; const std::array sizes{sceAgcDcbSetCxRegistersIndirectGetSize, sceAgcDcbSetShRegistersIndirectGetSize, sceAgcDcbSetUcRegistersIndirectGetSize}; @@ -239,6 +260,7 @@ int main() { testDirect(); testConfig(); testRangeFromAnyByte(); + testListFromAnyByte(); testIndirect(); testIndirectPlaceholder(); testDirectList(); From b5cccd562882bc1f5343a119f399564699279418 Mon Sep 17 00:00:00 2001 From: Shahul Mubeen A <174251444+shahulmubeen-a@users.noreply.github.com> Date: Thu, 22 Oct 2026 20:49:06 +0530 Subject: [PATCH 098/428] docs(libSceAgc): record that the console's register list alignment is unmeasured sceAgcCbSetShRegistersDirect and sceAgcCbSetUcRegistersDirect now accept a list at any byte address because PPSA26344 passes lists at 1 mod 4. The debt entry names that title evidence and states that how the console reads such a list is not measured, so the host behaviour is not taken for a measured console contract. Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libSceAgc.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index ab5eaacf4e..605947b24a 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -19,6 +19,7 @@ - [sceAgcDcbContextStateOp](../../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - delegates to `sceAgcDcbContextStateOp_0100` (same packets as KytyPS5's `AgcDcbContextStateOp`); KytyPS5 returns nullptr for an operation above 3, here it throws; the packet words were not compared with a real console - [sceAgcBranchPatchSetThenTarget_0300, sceAgcBranchPatchSetElseTarget_0300](../../../core/libs/prx/libSceAgc/Patch/src/Branch.cpp) (libSceAgc) - names from their NIDs (`7Wa3aeJgeVU`, `rP5xLdOf26k`); the `(packet, cachePolicy, target, sizeInDwords)` signature is inferred from PPSA28416's calls (a branch packet, 0, an aligned target, 4), with the policy stored where `sceAgcCbBranch` puts it - [sceAgcDcbSet{Cx,Sh,Uc}RegistersIndirectGetSize](../../../core/libs/prx/libSceAgc/DcbState/src/Registers.cpp) (libSceAgc) - the count argument is ignored: PPSA28416 calls them with leftover register contents (0xf5f5039c), so the real functions likely take none; the size is 20 bytes either way +- [sceAgcCbSetShRegistersDirect, sceAgcCbSetUcRegistersDirect](../../../core/libs/prx/libSceAgc/Cb/src/Registers.cpp) (libSceAgc) - a register list may start at any byte address, and its 8-byte `{offset, value}` entries are copied byte for byte. PPSA26344 (02.002.000) passes the `sh_registers` lists of compute shader headers that sit 1 byte past an 8-byte boundary in `core_common_subcore.core_b1_fx.sprig.xpps` ([#902](https://github.com/boykopovar/AnyPS5/pull/902)), so those lists start at 1 mod 4 (0x8021363131 in an open-world run). The title runs on the console with these lists, but how the console reads an unaligned list (at the given address, or at a rounded-down one) is not measured - [sceAgcUpdateInterpolantMapping_0100](../../../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - name from its NID (`k0E7vkgqAuE`); the `(regs, vs, ps)` arguments are the declared ones, behaviour unknown - [sceAgcWriteDataPatchSetDst, sceAgcWriteDataPatchSetCachePolicy and their Async forms](../../../core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp) (libSceAgc) - the signatures `(packet, uint8 value)` are inferred from the other patch functions, as no public declaration was found; the value is taken in the encoding of the `dst` and `cachePolicy` arguments of `sceAgcDcbWriteData` (graphics) and `sceAgcAcbWriteData` (Async, compute), and the patched packet is the one those calls would write. `sceAgcAsyncWriteDataPatchSetAddressOrOffset` is assumed to patch the address like `sceAgcWriteDataPatchSetAddressOrOffset`, as the compute WRITE_DATA packet keeps it in the same dwords - [sceAgcCreateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - name from its NID (`dbOlWdppb4o`); same arguments as `sceAgcCreateInterpolantMapping_0100`; PPSA26344 passes three pointers at each of nine observed call sites (`0x9F914E`, `0x9F91E2`, `0x9F9216`, `0x1604BBD`, `0x16122D7`, `0x1618285`, `0x161838C`, `0x16184EE`, `0x1618607`), consistent with that argument assumption, but does not read the return value there, so its return type is not established. The `SPI_PS_INPUT_CNTL` bits for `is_f16` 2 inputs follow KytyPS5, not confirmed on hardware From 8dea78c49020fde39e87370921d6af960a165bf7 Mon Sep 17 00:00:00 2001 From: Queuse <58829992+Queuse@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:25:35 -0300 Subject: [PATCH 099/428] docs(net): classify keepalive behavior debt --- docs/dev/debt/libSceNet.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index eeb9515008..4a8836e8dc 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -24,10 +24,11 @@ - [sceNetConfigRoutingStart](../../../core/libs/prx/libSceNet/Export.cpp): five pointer/scalar arguments are confirmed. Input address data is eight bytes; the mutable server parameter is 20 bytes and duplicates its first address when its count is one. Optional rules contain a count and pointer to 20-byte records with protocol, two address/port pairs and flags. Layouts are defined; rule flag meanings and protocol-specific port interpretation remain unresolved. - [sceNetConfigRoutingShowRoutingConfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the native routine consumes no arguments; the throwing implementation remains unchanged. -- [SO_KEEPALIVE](../../../core/libs/prx/libSceNet/Export.cpp) uses the host TCP keepalive timers and probe count, which can differ from the console. The boolean flag, bit-valued readback and truncated output follow FreeBSD 11.4's [sosetopt and sogetopt](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/kern/uipc_socket.c), without PS5 hardware verification. Non-stream sockets retain the flag without sending probes, as FreeBSD does. Null pointers retain the existing libSceNet `EINVAL` result instead of FreeBSD's `EFAULT`. ### Functional +- [SO_KEEPALIVE](../../../core/libs/prx/libSceNet/Export.cpp) uses the host TCP keepalive timers and probe count, which can differ from the console. The boolean flag, bit-valued readback and truncated output follow FreeBSD 11.4's [sosetopt and sogetopt](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/kern/uipc_socket.c), without PS5 hardware verification. Non-stream sockets retain the flag without sending probes, as FreeBSD does. Null pointers retain the existing libSceNet `EINVAL` result instead of FreeBSD's `EFAULT`. + - [sceNetResolverStartAton6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reverse-resolves a 16-byte IPv6 address through the host resolver (`getnameinfo` with `NI_NAMEREQD`) into the caller's buffer of `len` bytes, as `sceNetResolverStartAton` does for IPv4. The `(rid, addr, hostname, len, timeout, retry, flags)` signature comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L992-L997). An address with no name, or a name longer than `len - 1` bytes, fails with `RESOLVER_ENODNS` (`0x804101E1`) and leaves the buffer untouched; whether the console truncates instead is not verified. Abort flag 2 (ATON preservation) fails the next call with `EINTR` as it does `sceNetResolverStartAton`. Timeout and retry are ignored, and any nonzero flags throw because their meaning is unknown - [sceNetAccept](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) inherits the listener's non-blocking mode through the existing `SO_NBIO` setter, as the libkernel socket path does and FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L571) copies the socket state. The mode is sampled when accept starts; changes between connection creation and accept are not modelled. Not measured on a console. From 1ffb6a716382750b93ac335145f49ed0d1b12ff8 Mon Sep 17 00:00:00 2001 From: Queuse <58829992+Queuse@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:47:45 -0300 Subject: [PATCH 100/428] docs(net): avoid shared debt insertion point --- docs/dev/debt/libSceNet.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 4a8836e8dc..01863363b3 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -24,17 +24,15 @@ - [sceNetConfigRoutingStart](../../../core/libs/prx/libSceNet/Export.cpp): five pointer/scalar arguments are confirmed. Input address data is eight bytes; the mutable server parameter is 20 bytes and duplicates its first address when its count is one. Optional rules contain a count and pointer to 20-byte records with protocol, two address/port pairs and flags. Layouts are defined; rule flag meanings and protocol-specific port interpretation remain unresolved. - [sceNetConfigRoutingShowRoutingConfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the native routine consumes no arguments; the throwing implementation remains unchanged. - ### Functional -- [SO_KEEPALIVE](../../../core/libs/prx/libSceNet/Export.cpp) uses the host TCP keepalive timers and probe count, which can differ from the console. The boolean flag, bit-valued readback and truncated output follow FreeBSD 11.4's [sosetopt and sogetopt](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/kern/uipc_socket.c), without PS5 hardware verification. Non-stream sockets retain the flag without sending probes, as FreeBSD does. Null pointers retain the existing libSceNet `EINVAL` result instead of FreeBSD's `EFAULT`. - - [sceNetResolverStartAton6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reverse-resolves a 16-byte IPv6 address through the host resolver (`getnameinfo` with `NI_NAMEREQD`) into the caller's buffer of `len` bytes, as `sceNetResolverStartAton` does for IPv4. The `(rid, addr, hostname, len, timeout, retry, flags)` signature comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L992-L997). An address with no name, or a name longer than `len - 1` bytes, fails with `RESOLVER_ENODNS` (`0x804101E1`) and leaves the buffer untouched; whether the console truncates instead is not verified. Abort flag 2 (ATON preservation) fails the next call with `EINTR` as it does `sceNetResolverStartAton`. Timeout and retry are ignored, and any nonzero flags throw because their meaning is unknown - [sceNetAccept](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) inherits the listener's non-blocking mode through the existing `SO_NBIO` setter, as the libkernel socket path does and FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L571) copies the socket state. The mode is sampled when accept starts; changes between connection creation and accept are not modelled. Not measured on a console. - [sceNetConnect to AF_UNSPEC](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. Windows sockets stay non-blocking and the library waits for readiness until the stored timeout. A stream receive that times out still reports `ETIMEDOUT` rather than FreeBSD's `EAGAIN`; a datagram receive reports `EAGAIN`. Blocking stream sends return the queued byte count on timeout, or `EAGAIN` when no bytes were queued. - [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) take `SO_SNDTIMEO` and `SO_RCVTIMEO` (0x1005, 0x1006) as a 16-byte `struct timeval`, as FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/uipc_socket.c#L2657-L2690) do, and Sony's 0x1105 and 0x1106 as an `int` of microseconds. A [console log of ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/issues/49) (firmware 12.20) shows receives timing out every 100 ms after it gave 0x1006 a `{0, 100000}` timeval. The errors for a value shorter than a timeval (`EINVAL`) or out of range (`EDOM`), and `sceNetGetsockopt` cutting the timeval to the caller's length, follow FreeBSD's `sooptcopyin` and `sooptcopyout` and are not measured on a console +- [SO_KEEPALIVE](../../../core/libs/prx/libSceNet/Export.cpp) uses the host TCP keepalive timers and probe count, which can differ from the console. The boolean flag, bit-valued readback and truncated output follow FreeBSD 11.4's [sosetopt and sogetopt](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/kern/uipc_socket.c), without PS5 hardware verification. Non-stream sockets retain the flag without sending probes, as FreeBSD does. Null pointers retain the existing libSceNet `EINVAL` result instead of FreeBSD's `EFAULT`. - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) stores `SO_ONESBCAST` (`0x10000`, [shadPS4's `ORBIS_NET_SO_ONESBCAST`](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net_types.h)) per socket and `sceNetGetsockopt` reads back the value that was set, as shadPS4 does; PPSA34501 sets it and crashes later when it fails with `EOPNOTSUPP`. Sends ignore it. FreeBSD 11.4's [ip(4)](https://man.freebsd.org/cgi/man.cgi?manpath=FreeBSD+11.4-RELEASE&query=ip&sektion=4) documents the like-named `IPPROTO_IP` option `IP_ONESBCAST`: a datagram sent to an interface's directed broadcast address, such as 192.168.2.255, leaves through that interface with its destination rewritten to 255.255.255.255, and only on a socket allowed to set `SO_BROADCAST`. Here host routing selects the outgoing interface and the destination is not rewritten. shadPS4 also leaves sends unchanged, and its [TODO](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp#L2467-L2470) reads the option the other way round: a send to 255.255.255.255 would be converted to the interface's broadcast address. Whether the console's `SOL_SOCKET` option behaves like `IP_ONESBCAST`, and what it reads back, is not verified on hardware. FreeBSD 11.4's `sogetopt()` reads several boolean socket options back as their flag bit (`so_options & optname`, `sys/kern/uipc_socket.c`), not as the value set; whether the console does so for `0x10000` is unknown - [sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reads `SO_ERROR` from the host socket. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `sceNetConnect`; a second failure with the same code on the same socket without a `sceNetConnect` in between would be hidden. - [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) set `TCP_NODELAY` (`IPPROTO_TCP`, 1) on the host socket of a stream socket and read it back as `TF_NODELAY` (4) or 0, as FreeBSD 11.4's [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1626-L1650) does. Every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_NOPUSH` and `TCP_KEEPIDLE` among them, which FreeBSD implements) fails with `ENOPROTOOPT`, and the `IPPROTO_TCP` level on a socket that is not a stream socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/ip_output.c#L943-L944) answers it. A set on a connection that FreeBSD's [`tcp_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1498-L1502) refuses with `ECONNRESET` (refused, reset or in `TIME_WAIT`) succeeds here because the host accepts it. A null value with a nonzero length fails with `EINVAL`, where FreeBSD's `kern_setsockopt` returns `EFAULT`, and a get into less than an `int` fails with `EINVAL`, where `sooptcopyout` truncates. None of this was measured on a console. From f696ec880990e282ba225d53d5f90175920964f7 Mon Sep 17 00:00:00 2001 From: Queuse <58829992+Queuse@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:55:52 -0300 Subject: [PATCH 101/428] docs(net): keep independent option entries apart --- docs/dev/debt/libSceNet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 01863363b3..4a77272617 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -32,9 +32,9 @@ - [sceNetConnect to AF_UNSPEC](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. Windows sockets stay non-blocking and the library waits for readiness until the stored timeout. A stream receive that times out still reports `ETIMEDOUT` rather than FreeBSD's `EAGAIN`; a datagram receive reports `EAGAIN`. Blocking stream sends return the queued byte count on timeout, or `EAGAIN` when no bytes were queued. - [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) take `SO_SNDTIMEO` and `SO_RCVTIMEO` (0x1005, 0x1006) as a 16-byte `struct timeval`, as FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/uipc_socket.c#L2657-L2690) do, and Sony's 0x1105 and 0x1106 as an `int` of microseconds. A [console log of ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/issues/49) (firmware 12.20) shows receives timing out every 100 ms after it gave 0x1006 a `{0, 100000}` timeval. The errors for a value shorter than a timeval (`EINVAL`) or out of range (`EDOM`), and `sceNetGetsockopt` cutting the timeval to the caller's length, follow FreeBSD's `sooptcopyin` and `sooptcopyout` and are not measured on a console -- [SO_KEEPALIVE](../../../core/libs/prx/libSceNet/Export.cpp) uses the host TCP keepalive timers and probe count, which can differ from the console. The boolean flag, bit-valued readback and truncated output follow FreeBSD 11.4's [sosetopt and sogetopt](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/kern/uipc_socket.c), without PS5 hardware verification. Non-stream sockets retain the flag without sending probes, as FreeBSD does. Null pointers retain the existing libSceNet `EINVAL` result instead of FreeBSD's `EFAULT`. - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) stores `SO_ONESBCAST` (`0x10000`, [shadPS4's `ORBIS_NET_SO_ONESBCAST`](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net_types.h)) per socket and `sceNetGetsockopt` reads back the value that was set, as shadPS4 does; PPSA34501 sets it and crashes later when it fails with `EOPNOTSUPP`. Sends ignore it. FreeBSD 11.4's [ip(4)](https://man.freebsd.org/cgi/man.cgi?manpath=FreeBSD+11.4-RELEASE&query=ip&sektion=4) documents the like-named `IPPROTO_IP` option `IP_ONESBCAST`: a datagram sent to an interface's directed broadcast address, such as 192.168.2.255, leaves through that interface with its destination rewritten to 255.255.255.255, and only on a socket allowed to set `SO_BROADCAST`. Here host routing selects the outgoing interface and the destination is not rewritten. shadPS4 also leaves sends unchanged, and its [TODO](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp#L2467-L2470) reads the option the other way round: a send to 255.255.255.255 would be converted to the interface's broadcast address. Whether the console's `SOL_SOCKET` option behaves like `IP_ONESBCAST`, and what it reads back, is not verified on hardware. FreeBSD 11.4's `sogetopt()` reads several boolean socket options back as their flag bit (`so_options & optname`, `sys/kern/uipc_socket.c`), not as the value set; whether the console does so for `0x10000` is unknown - [sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reads `SO_ERROR` from the host socket. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `sceNetConnect`; a second failure with the same code on the same socket without a `sceNetConnect` in between would be hidden. +- [SO_KEEPALIVE](../../../core/libs/prx/libSceNet/Export.cpp) uses the host TCP keepalive timers and probe count, which can differ from the console. The boolean flag, bit-valued readback and truncated output follow FreeBSD 11.4's [sosetopt and sogetopt](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/kern/uipc_socket.c), without PS5 hardware verification. Non-stream sockets retain the flag without sending probes, as FreeBSD does. Null pointers retain the existing libSceNet `EINVAL` result instead of FreeBSD's `EFAULT`. - [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) set `TCP_NODELAY` (`IPPROTO_TCP`, 1) on the host socket of a stream socket and read it back as `TF_NODELAY` (4) or 0, as FreeBSD 11.4's [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1626-L1650) does. Every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_NOPUSH` and `TCP_KEEPIDLE` among them, which FreeBSD implements) fails with `ENOPROTOOPT`, and the `IPPROTO_TCP` level on a socket that is not a stream socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/ip_output.c#L943-L944) answers it. A set on a connection that FreeBSD's [`tcp_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1498-L1502) refuses with `ECONNRESET` (refused, reset or in `TIME_WAIT`) succeeds here because the host accepts it. A null value with a nonzero length fails with `EINVAL`, where FreeBSD's `kern_setsockopt` returns `EFAULT`, and a get into less than an `int` fails with `EINVAL`, where `sooptcopyout` truncates. None of this was measured on a console. - [sceNetGetSockInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reports 0 for the name, pid, policy, priority, queue lengths, virtual ports, state, bandwidth, drops and waits, sets no status flag except `SELF` (0x1) and `NONBLOCK` (0x200000), and reports the host's buffer sizes where none was set. An IPv6 socket fails with `EAFNOSUPPORT` and is not counted or listed; shadPS4 lists it with zero addresses. `flags` bits other than 0x40 fail with `EINVAL`, where shadPS4 ignores `flags`; 0x40 adds sockets in TIME_WAIT ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs)), but closed sockets are not kept. Otherwise the `s` below 0 count and list, the return value and `EINVAL` for a null `info` or an `n` below 1 follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/06e813ff8c8e2a6d81b02cabfcc9543c3debf78e/src/core/libraries/net/net.cpp#L1498-L1522). Not measured on a console. - [sceNetSendmsg and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported From 72d856954eb33571e0cbf95e8ad32682fc5af71c Mon Sep 17 00:00:00 2001 From: Sagar Thakrar Date: Thu, 22 Oct 2026 00:59:26 +0530 Subject: [PATCH 102/428] perf(relinker): cache canonical output paths during collision checks --- core/relinker/main.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 98bca27396..b314bc894c 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -122,9 +122,10 @@ bool SameOutputFile(const std::filesystem::path& first, const std::filesystem::p return Io::SameFile(first, second); } -bool SameOutputDestination(const std::filesystem::path& first, const std::filesystem::path& second) { - const auto firstPath = std::filesystem::weakly_canonical(first); - const auto secondPath = std::filesystem::weakly_canonical(second); +bool SameOutputDestination(const std::filesystem::path& first, const std::filesystem::path& second, + std::filesystem::path& firstPath, std::filesystem::path& secondPath) { + if (firstPath.empty()) firstPath = std::filesystem::weakly_canonical(first); + if (secondPath.empty()) secondPath = std::filesystem::weakly_canonical(second); if (firstPath == secondPath) return true; if (std::filesystem::exists(first) && std::filesystem::exists(second)) return SameOutputFile(first, second); #ifdef _WIN32 @@ -394,11 +395,12 @@ int Run(const int argc, char* argv[]) { outputPaths.push_back(desktopEntry->EntryPath); } if (args.windowsDiagnostics) outputPaths.push_back(std::filesystem::absolute("windows-diagnostics-imports.txt")); + std::vector canonicalOutputPaths(outputPaths.size()); for (std::size_t index = 0; index < outputPaths.size(); ++index) { for (std::size_t previous = 0; previous < index; ++previous) { const auto& first = outputPaths[index]; const auto& second = outputPaths[previous]; - if (SameOutputDestination(first, second)) + if (SameOutputDestination(first, second, canonicalOutputPaths[index], canonicalOutputPaths[previous])) throw Domain::RelinkerException("Output paths collide: " + Io::Utf8Path(first) + " and " + Io::Utf8Path(second)); } } From 8029493488844f1642c71526e31aacc1b2d06522 Mon Sep 17 00:00:00 2001 From: KernelPanicNT Date: Wed, 21 Oct 2026 20:36:40 +0000 Subject: [PATCH 103/428] fix(libSceNpManager): return the module error for an out-of-range state callback id sceNpUnregisterStateCallbackA returns 0x80550015 for a callback id outside 1..8, compared unsigned so 0, negative ids and INT_MIN are included, as libSceNpManager.sprx does at RVA 0x15b10 in prosper's reading. An empty slot still returns 0x80550009. --- core/libs/prx/libSceNpManager/Export.cpp | 3 ++- core/libs/tests/GuestNpManager.cpp | 9 +++++++-- docs/dev/debt/libSceNpManager.md | 3 ++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index 93a1095e2e..8d73f947cb 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -16,6 +16,7 @@ static constexpr int SCE_NP_ERROR_CALLBACK_ALREADY_REGISTERED = static_cast static constexpr int SCE_NP_ERROR_CALLBACK_NOT_REGISTERED = static_cast(0x80550009); static constexpr int SCE_NP_ERROR_INVALID_SIZE = static_cast(0x80550011); static constexpr int SCE_NP_ERROR_REQUEST_NOT_FOUND = static_cast(0x80550014); +static constexpr int SCE_NP_ERROR_INVALID_ID = static_cast(0x80550015); static constexpr int SCE_NP_ERROR_CALLBACK_MAX = static_cast(0x8055001D); static constexpr uint32_t NP_STATE_SIGNED_OUT = 1; static constexpr int NP_POLL_ASYNC_FINISHED = 0; @@ -945,7 +946,7 @@ int APS5_VABI sceNpRegisterStateCallbackA(NpStateCallbackA callback, void* userd } int APS5_VABI sceNpUnregisterStateCallbackA(int callbackId) { - if (callbackId < 1 || callbackId > NP_STATE_CALLBACK_MAX) return SCE_NP_ERROR_INVALID_ARGUMENT; + if (static_cast(callbackId) - 1u >= static_cast(NP_STATE_CALLBACK_MAX)) return SCE_NP_ERROR_INVALID_ID; std::lock_guard lock(stateMutex); auto& entry = stateCallbacksA[callbackId - 1]; if (!entry.callback) return SCE_NP_ERROR_CALLBACK_NOT_REGISTERED; diff --git a/core/libs/tests/GuestNpManager.cpp b/core/libs/tests/GuestNpManager.cpp index 9c51efbe00..ec499427b7 100644 --- a/core/libs/tests/GuestNpManager.cpp +++ b/core/libs/tests/GuestNpManager.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -58,6 +59,7 @@ constexpr int UserNotFound = static_cast(0x80550007u); constexpr int AlreadyRegistered = static_cast(0x80550008u); constexpr int NotRegistered = static_cast(0x80550009u); constexpr int RequestNotFound = static_cast(0x80550014u); +constexpr int InvalidId = static_cast(0x80550015u); void Require(bool condition, const char* message) { if (!condition) { @@ -152,8 +154,11 @@ void CheckStateCallbacksA() { Require(SignedOutOnce(stateCallsA[index], &userdata[index]), "each callback gets SIGNED_OUT once"); } Require(stateCallsA[8].calls == 0, "a rejected callback was called"); - Require(sceNpUnregisterStateCallbackA(0) == InvalidArgument, "callback id 0"); - Require(sceNpUnregisterStateCallbackA(9) == InvalidArgument, "callback id 9"); + Require(sceNpUnregisterStateCallbackA(0) == InvalidId, "callback id 0"); + Require(sceNpUnregisterStateCallbackA(9) == InvalidId, "callback id 9"); + Require(sceNpUnregisterStateCallbackA(-1) == InvalidId, "callback id -1"); + Require(sceNpUnregisterStateCallbackA(std::numeric_limits::min()) == InvalidId, "callback id INT_MIN"); + Require(sceNpUnregisterStateCallbackA(std::numeric_limits::max()) == InvalidId, "callback id INT_MAX"); Require(sceNpUnregisterStateCallbackA(ids[3]) == 0, "unregistering one state callback"); for (int index = 0; index < 8; ++index) { if (index == 3) continue; diff --git a/docs/dev/debt/libSceNpManager.md b/docs/dev/debt/libSceNpManager.md index 96633c6c2b..04d4c88b42 100644 --- a/docs/dev/debt/libSceNpManager.md +++ b/docs/dev/debt/libSceNpManager.md @@ -23,7 +23,8 @@ - [sceNpGetAccountId, sceNpSetGamePresenceOnline](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - PS4 forms; the `(online id, account id output)` and `(request id, online id, presence)` signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp#L1092-L1096). A null online id or output, or request id 0 for `sceNpSetGamePresenceOnline`, returns `SCE_NP_ERROR_INVALID_ARGUMENT`. `sceNpSetGamePresenceOnline` then returns `SCE_NP_ERROR_REQUEST_NOT_FOUND` for a request that was never created or was deleted, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c1b26558352b863f80142256e4df38585aa7d727/src/core/libraries/np/np_manager.cpp#L1098-L1104) does. Otherwise both return `SCE_NP_ERROR_SIGNED_OUT` without writing, as `sceNpGetAccountIdA` does. The console's result for a signed-out user is not measured - [NP reachability callbacks](../../../core/libs/prx/libSceNpManager/Export.cpp) retain one callback and its user data; duplicate registration and unregistering an absent callback return the errors used by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/np/np_manager.cpp). No callback is delivered because the existing NP model remains signed out with reachability unavailable. - [sceNpCheckNpAvailability](../../../core/libs/prx/libSceNpManager/Export.cpp): native NP initialization and online-ID lookup are not modeled. The offline host returns user-not-found for valid arguments after clearing the optional one-byte availability output; request ID 0 and a null online-ID pointer return invalid-argument before writing the output. -- [NP state callbacks](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the registration rules are [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L857-L901)'s: `sceNpRegisterStateCallbackA` keeps up to 8 callbacks and returns the slot plus 1, a null callback returns `INVALID_ARGUMENT`, a callback already registered `CALLBACK_ALREADY_REGISTERED` and a ninth one `CALLBACK_MAX` (0x8055001D, [np_error.h](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_error.h#L38)); `sceNpUnregisterStateCallbackA` returns `INVALID_ARGUMENT` for an id outside 1..8 and `CALLBACK_NOT_REGISTERED` for an empty slot; the legacy [sceNpRegisterStateCallback](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L981-L1005) keeps one callback with the same errors. Each registration gets `SIGNED_OUT` for the initial user (0x10000000) once, from the next `sceNpCheckCallback` on the calling thread. For the A form this follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L3959-L3974), whose [sceNpRegisterStateCallbackA](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4021-L4034) marks the registration pending. KytyPS5 never calls the legacy callback ([sceNpRegisterStateCallback](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4009-L4019) stores nothing); it is called as `(user, SIGNED_OUT, nullptr, userdata)`, as in shadPS4, which passes an NpId only for `SIGNED_IN` ([L823](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L823), [L938-L940](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L938-L940)). shadPS4 instead queues `SIGNED_OUT` from the user service login event and delivers it from `sceNpCheckCallback` ([DispatchPendingNpStateCallbacks](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L903-L962)); the reachability callback is not given an initial `UNAVAILABLE`, unlike that function ([L922-L961](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L922-L961)). A callback being delivered when another thread unregisters it is still called once. When and how often the console calls the state callbacks, and the errors, are not measured +- [NP state callbacks](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the registration rules are [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L857-L901)'s: `sceNpRegisterStateCallbackA` keeps up to 8 callbacks and returns the slot plus 1, a null callback returns `INVALID_ARGUMENT`, a callback already registered `CALLBACK_ALREADY_REGISTERED` and a ninth one `CALLBACK_MAX` (0x8055001D, [np_error.h](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_error.h#L38)); the unregister errors are in the next entry; the legacy [sceNpRegisterStateCallback](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L981-L1005) keeps one callback with the same errors. Each registration gets `SIGNED_OUT` for the initial user (0x10000000) once, from the next `sceNpCheckCallback` on the calling thread. For the A form this follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L3959-L3974), whose [sceNpRegisterStateCallbackA](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4021-L4034) marks the registration pending. KytyPS5 never calls the legacy callback ([sceNpRegisterStateCallback](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4009-L4019) stores nothing); it is called as `(user, SIGNED_OUT, nullptr, userdata)`, as in shadPS4, which passes an NpId only for `SIGNED_IN` ([L823](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L823), [L938-L940](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L938-L940)). shadPS4 instead queues `SIGNED_OUT` from the user service login event and delivers it from `sceNpCheckCallback` ([DispatchPendingNpStateCallbacks](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L903-L962)); the reachability callback is not given an initial `UNAVAILABLE`, unlike that function ([L922-L961](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L922-L961)). A callback being delivered when another thread unregisters it is still called once. When and how often the console calls the state callbacks, and the errors, are not measured +- [sceNpRegisterStateCallbackA and sceNpUnregisterStateCallbackA](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - never return the module's `0x80550002` for a failed [readiness check](https://github.com/mattias800/prosper/blob/6c09a34a092a00898fcead5f855200ca7c368cd2/prosper/src/hle/np/state_callbacks.cpp#L152-L153), whose input is not known, and are not measured on a PS5. An unregister id outside 1..8, compared unsigned so 0 and negative ids are included, returns `INVALID_ID` (0x80550015) and an empty slot `CALLBACK_NOT_REGISTERED`, as [prosper](https://github.com/mattias800/prosper/blob/6c09a34a092a00898fcead5f855200ca7c368cd2/prosper/src/hle/np/state_callbacks.cpp#L176-L188) reads `libSceNpManager.sprx` at RVA 0x15b10, without recording its firmware version; the same reading gives the registration rules above ([L162-L175](https://github.com/mattias800/prosper/blob/6c09a34a092a00898fcead5f855200ca7c368cd2/prosper/src/hle/np/state_callbacks.cpp#L162-L175)) - [sceNpRegisterGamePresenceCallback](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - accepts any callback, null included, and returns nothing; no game presence event is delivered because the NP model remains signed out. [Kyty](https://github.com/InoriRus/Kyty/blob/4733b7e1c91b10554a52007903d74dc76c39a230/source/emulator/src/Network.cpp#L1549-L1552) ignores the callback the same way, and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/np/np_manager.cpp#L1121-L1126) keeps the last one without calling it. Not measured on a PS5 - [sceNpIsPlusMember and sceNpSetContentRestriction](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the signatures and checks follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_manager.cpp): a null output or restriction is `INVALID_ARGUMENT`, a restriction size other than 0x18 is `INVALID_SIZE` (0x80550011), and a negative default age, more than 256 age restrictions or a count without an array is `INVALID_ARGUMENT`. Any user is reported as not a Plus member, and a valid restriction is accepted and not stored. Not measured on a PS5 From 587cd6433123a9bcbe9dad4e3904686cee0cb4a2 Mon Sep 17 00:00:00 2001 From: cadenasynyster <8373765+cadenasynyster@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:00:38 +0200 Subject: [PATCH 104/428] test(libSceSaveData): create the mount identity link with CreateSymbolicLinkW on Windows MinGW's std::filesystem::create_directory_symlink always fails on Windows, so CheckEquivalentMounts never ran its symbolic link alias there and only checked the case alias. It now creates and removes the link with CreateSymbolicLinkW and RemoveDirectoryW, as SaveDataFileWriteFailure.cpp does. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GtXavMkgCTCdmRpweviHcQ --- core/libs/tests/GuestSaveDataMountStatus.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/core/libs/tests/GuestSaveDataMountStatus.cpp b/core/libs/tests/GuestSaveDataMountStatus.cpp index 88557e574d..53da76987f 100644 --- a/core/libs/tests/GuestSaveDataMountStatus.cpp +++ b/core/libs/tests/GuestSaveDataMountStatus.cpp @@ -7,6 +7,9 @@ #include #include #include +#ifdef _WIN32 +#include +#endif extern "C" { int APS5_VABI sceSaveDataInitialize3(const void*); @@ -157,7 +160,15 @@ void CheckEquivalentMounts() { const auto casePath = std::filesystem::path("_sd") / "IDENTITY"; const bool caseAlias = std::filesystem::exists(casePath) && std::filesystem::equivalent(originalPath, casePath, error); Check(!error || error == std::errc::no_such_file_or_directory, "case-alias identity check succeeds"); +#ifdef _WIN32 + error.clear(); + if (!CreateSymbolicLinkW((std::filesystem::path("_sd") / "linked").c_str(), L"identity", + SYMBOLIC_LINK_FLAG_DIRECTORY | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE)) { + error = std::error_code(GetLastError(), std::system_category()); + } +#else std::filesystem::create_directory_symlink("identity", std::filesystem::path("_sd") / "linked", error); +#endif const bool symlinkAlias = !error; Check(caseAlias || symlinkAlias, "a host alias is available to check mount identity"); @@ -179,7 +190,12 @@ void CheckEquivalentMounts() { if (caseAlias) MountAndCheck("IDENTITY", MountReadWrite, 0u, "case alias after unmount"); if (symlinkAlias) { MountAndCheck("linked", MountReadWrite, 0u, "symlink alias after unmount"); +#ifdef _WIN32 + error.clear(); + if (!RemoveDirectoryW((std::filesystem::path("_sd") / "linked").c_str())) error = std::error_code(GetLastError(), std::system_category()); +#else std::filesystem::remove(std::filesystem::path("_sd") / "linked", error); +#endif Check(!error, "symlink alias is removed"); } } From 84fc3594b8edd99c9468850381f1ec8df8e95f62 Mon Sep 17 00:00:00 2001 From: cadenasynyster <8373765+cadenasynyster@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:31:35 +0200 Subject: [PATCH 105/428] test(libkernel): remove a leftover kernel_fcntl_probe.bin before guest_kernel_fcntl opens it FileFlagsRoundTrip opens kernel_fcntl_probe.bin with O_CREAT but without O_TRUNC and removes it only when every check passes. A run that aborts leaves the file behind, and every later run then fails at the FIONREAD check because the file is longer than the five bytes the test writes. The test now removes the file before opening it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GtXavMkgCTCdmRpweviHcQ --- core/libs/tests/GuestKernelFcntl.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index b75d3c22a9..e7f8e62036 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -199,6 +199,7 @@ static void SocketsKeepTheirPath() { } static void FileFlagsRoundTrip() { + std::remove("kernel_fcntl_probe.bin"); const int file = sceKernelOpen("kernel_fcntl_probe.bin", CreateReadWrite, 0644); Require(file >= 0); Require(sceKernelFcntl(file, GetFlags) == ReadWrite); From 2eedb856d938adb87b079f78005e332d2e252274 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Thu, 22 Oct 2026 23:50:24 +0200 Subject: [PATCH 106/428] fix(libSceNgs2): return INVALID_NUM_CHANNELS for PLAY on a sampler voice that was never set up A sampler voice with no setup used to enter the playing state and never finish, so a title that frees its sound slots by polling sceNgs2VoiceGetStateFlags ran out of slots. The PS4 firmware 11.00 library fails the event and keeps the voice idle. Co-Authored-By: Claude Sonnet 5.5 --- .../libs/prx/libSceNgs2.native/CMakeLists.txt | 2 +- .../libSceNgs2.native/include/Ngs2Types.hpp | 1 + core/libs/prx/libSceNgs2.native/src/Voice.cpp | 53 ++++++++---- .../tests/Ngs2VoiceEvents.cpp | 82 +++++++++++++++++++ docs/dev/debt/libSceNgs2.native.md | 1 + 5 files changed, 120 insertions(+), 19 deletions(-) create mode 100644 core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceEvents.cpp diff --git a/core/libs/prx/libSceNgs2.native/CMakeLists.txt b/core/libs/prx/libSceNgs2.native/CMakeLists.txt index 93bb6c445e..fddd6277ab 100644 --- a/core/libs/prx/libSceNgs2.native/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2.native/CMakeLists.txt @@ -2,7 +2,7 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/Ngs2Library.cmake) add_sce_ngs2_library(libSceNgs2.native) if(BUILD_TESTING) - foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse Stream UserFx Report Geom VoiceIndex HandleErrors VoiceHandles) + foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse Stream UserFx Report Geom VoiceIndex HandleErrors VoiceHandles VoiceEvents) string(TOLOWER ${ngs2Test} testName) add_test_executable(ngs2_${testName}_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Ngs2${ngs2Test}.cpp) target_include_directories(ngs2_${testName}_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index d113635624..c254121f1b 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -9,6 +9,7 @@ static constexpr int SCE_NGS2_OK = 0; static constexpr int SCE_NGS2_ERROR_INVALID_OUT_ADDRESS = static_cast(0x804A8010); static constexpr int SCE_NGS2_ERROR_INVALID_OUT_SIZE = static_cast(0x804A8011); +static constexpr int SCE_NGS2_ERROR_INVALID_NUM_CHANNELS = static_cast(0x804A8052); static constexpr int SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE = static_cast(0x804A8201); static constexpr int SCE_NGS2_ERROR_INVALID_RACK_HANDLE = static_cast(0x804A8202); static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_INDEX = static_cast(0x804A8305); diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index bc19ab0a0a..a1d2dafeff 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -20,6 +20,15 @@ static constexpr std::uint8_t COMMAND_TYPE_UINT = 4; static constexpr std::uint8_t COMMAND_TYPE_FLOAT_ARRAY = 0x11; static constexpr float MAX_MATRIX_LEVEL = 4.0f; +struct Ngs2ReturnCode { + int code; +}; + +static void SendEvent(Ngs2Voice& voice, std::uint32_t eventId) { + if (eventId == SCE_NGS2_VOICE_EVENT_PLAY && voice.rack->rackId == SCE_NGS2_RACK_ID_SAMPLER && voice.channels == 0) throw Ngs2ReturnCode{SCE_NGS2_ERROR_INVALID_NUM_CHANNELS}; + voice.SetEvent(eventId); +} + void Ngs2Voice::SetEvent(std::uint32_t eventId) { switch (eventId) { case SCE_NGS2_VOICE_EVENT_PLAY: @@ -158,7 +167,7 @@ static void ApplyCommonParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param break; } case SCE_NGS2_VOICE_PARAM_PATCH: Patch(voice, ParamAs(param)); break; - case SCE_NGS2_VOICE_PARAM_EVENT: voice.SetEvent(ParamAs(param).event_id); break; + case SCE_NGS2_VOICE_PARAM_EVENT: SendEvent(voice, ParamAs(param).event_id); break; case SCE_NGS2_VOICE_PARAM_CALLBACK: { const auto& callback = ParamAs(param); voice.callback = callback.callback; @@ -400,9 +409,13 @@ int APS5_VABI sceNgs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamHe auto* found = Ngs2FindVoice(voice_handle); if (found == nullptr) return SCE_NGS2_ERROR_INVALID_VOICE_HANDLE; auto& voice = *found; - for (const auto* param = param_list;; param = reinterpret_cast(reinterpret_cast(param) + param->next)) { - ApplyParam(voice, *param); - if (param->next == 0) break; + try { + for (const auto* param = param_list;; param = reinterpret_cast(reinterpret_cast(param) + param->next)) { + ApplyParam(voice, *param); + if (param->next == 0) break; + } + } catch (const Ngs2ReturnCode& result) { + return result.code; } return SCE_NGS2_OK; } @@ -413,20 +426,24 @@ int APS5_VABI sceNgs2VoiceRunCommands(uintptr_t voice_handle, const Ngs2VoiceCom if (found == nullptr) return SCE_NGS2_ERROR_INVALID_VOICE_HANDLE; auto& voice = *found; if (commands == nullptr && num_commands != 0) APS5_INVALID_ARG_EX; - for (std::size_t i = 0; i < num_commands; i++) { - const auto& command = commands[i]; - const std::uint32_t index = command.id >> 24; - const std::uint32_t id = command.id & 0xffffff; - const std::uint8_t expectedType = id == COMMAND_EVENT ? COMMAND_TYPE_UINT - : id == COMMAND_MATRIX_LEVELS ? COMMAND_TYPE_FLOAT_ARRAY - : id == COMMAND_PORT_VOLUME ? COMMAND_TYPE_FLOAT - : id == COMMAND_PORT_MATRIX ? COMMAND_TYPE_INT : 0; - if (expectedType == 0) throw std::runtime_error("NGS2: voice command " + Ngs2Hex(command.id) + " is not implemented"); - if (command.type != expectedType) throw std::invalid_argument("NGS2: voice command " + Ngs2Hex(command.id) + " has value type " + Ngs2Hex(command.type)); - if (id == COMMAND_EVENT) voice.SetEvent(command.value.u); - else if (id == COMMAND_MATRIX_LEVELS) SetMatrixLevels(voice, index, command.value.levels, command.count); - else if (id == COMMAND_PORT_VOLUME) PortAt(voice, index).volume = command.value.f; - else SetPortMatrix(voice, index, command.value.i); + try { + for (std::size_t i = 0; i < num_commands; i++) { + const auto& command = commands[i]; + const std::uint32_t index = command.id >> 24; + const std::uint32_t id = command.id & 0xffffff; + const std::uint8_t expectedType = id == COMMAND_EVENT ? COMMAND_TYPE_UINT + : id == COMMAND_MATRIX_LEVELS ? COMMAND_TYPE_FLOAT_ARRAY + : id == COMMAND_PORT_VOLUME ? COMMAND_TYPE_FLOAT + : id == COMMAND_PORT_MATRIX ? COMMAND_TYPE_INT : 0; + if (expectedType == 0) throw std::runtime_error("NGS2: voice command " + Ngs2Hex(command.id) + " is not implemented"); + if (command.type != expectedType) throw std::invalid_argument("NGS2: voice command " + Ngs2Hex(command.id) + " has value type " + Ngs2Hex(command.type)); + if (id == COMMAND_EVENT) SendEvent(voice, command.value.u); + else if (id == COMMAND_MATRIX_LEVELS) SetMatrixLevels(voice, index, command.value.levels, command.count); + else if (id == COMMAND_PORT_VOLUME) PortAt(voice, index).volume = command.value.f; + else SetPortMatrix(voice, index, command.value.i); + } + } catch (const Ngs2ReturnCode& result) { + return result.code; } return SCE_NGS2_OK; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceEvents.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceEvents.cpp new file mode 100644 index 0000000000..5ad7626cd8 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceEvents.cpp @@ -0,0 +1,82 @@ +#include "Ngs2Test.hpp" + +#include +#include + +static const Ngs2SamplerVoiceSetupParam SamplerSetup{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1, 48000, 0, 0, 0}}; + +static Ngs2VoiceEventParam PlayParam() { + return {{sizeof(Ngs2VoiceEventParam), 0, SCE_NGS2_VOICE_PARAM_EVENT}, SCE_NGS2_VOICE_EVENT_PLAY}; +} + +static uintptr_t Sampler(uintptr_t system) { + const auto master = Mastering(system, 1); + const auto voice = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER)); + Patch(voice, master); + return voice; +} + +static void TestPlayWithoutSetup() { + const auto system = CreateSystem(); + const auto voice = Sampler(system); + const auto play = PlayParam(); + const Ngs2VoiceCommand playCommand{2, 0, 4, 0, {.u = SCE_NGS2_VOICE_EVENT_PLAY}}; + Event(voice, SCE_NGS2_VOICE_EVENT_STOP_IMM); + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, nullptr, 0, 0, nullptr}); + Require(sceNgs2VoiceControl(voice, &play.header) == SCE_NGS2_ERROR_INVALID_NUM_CHANNELS); + Require(sceNgs2VoiceRunCommands(voice, &playCommand, 1) == SCE_NGS2_ERROR_INVALID_NUM_CHANNELS); + + std::vector out(Grain, -1.0f); + const Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 1}; + for (int grain = 0; grain < 3; grain++) { + Require(Flags(voice) == 0); + Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); + for (float sample : out) Require(sample == 0.0f); + } + Require(Flags(voice) == 0); + + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, SamplerSetup); + Require(sceNgs2VoiceRunCommands(voice, &playCommand, 1) == SCE_NGS2_OK); + Require(Flags(voice) == SCE_NGS2_VOICE_STATE_FLAG_INUSE); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestPlayAfterSetupInOneList() { + const auto system = CreateSystem(); + const auto voice = Sampler(system); + struct List { + Ngs2SamplerVoiceSetupParam setup; + Ngs2VoiceEventParam play; + }; + List list{SamplerSetup, PlayParam()}; + list.setup.header = {sizeof(list.setup), static_cast(sizeof(list.setup)), SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP}; + Require(sceNgs2VoiceControl(voice, &list.setup.header) == SCE_NGS2_OK); + Require(Flags(voice) == SCE_NGS2_VOICE_STATE_FLAG_INUSE); + + const auto other = Sampler(system); + struct Reversed { + Ngs2VoiceEventParam play; + Ngs2SamplerVoiceSetupParam setup; + }; + Reversed reversed{PlayParam(), SamplerSetup}; + reversed.play.header = {sizeof(reversed.play), static_cast(sizeof(reversed.play)), SCE_NGS2_VOICE_PARAM_EVENT}; + reversed.setup.header = {sizeof(reversed.setup), 0, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP}; + Require(sceNgs2VoiceControl(other, &reversed.play.header) == SCE_NGS2_ERROR_INVALID_NUM_CHANNELS); + Require(Flags(other) == 0); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestOtherRacksStillPlay() { + const auto system = CreateSystem(); + const auto submixer = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SUBMIXER)); + Event(submixer, SCE_NGS2_VOICE_EVENT_PLAY); + Require(Flags(submixer) == SCE_NGS2_VOICE_STATE_FLAG_INUSE); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +int main() { + TestPlayWithoutSetup(); + TestPlayAfterSetupInOneList(); + TestOtherRacksStillPlay(); + return 0; +} diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index b26266e77b..6952c9a55c 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -14,6 +14,7 @@ - [NGS2 mastering voice gain](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - the param id 0x30000004 is taken from PPSA22520 and the layout from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2MasteringVoiceGainParam`; no reference gives the units or range. The levels are assumed to be linear factors applied when the mastering voice is mixed into the render buffer, so the voice's own samples stay unscaled; the LFE level applies to channel 3 of 6- and 8-channel voices, assuming the L R C LFE order of the AudioOut2 entries above, and the full-band level to the rest. Negative levels are accepted, and a new setup resets both levels to 1 - [NGS2 stereo mastering voice into a surround buffer](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - PPSA21402 renders a 2-channel mastering voice into a 6- or 8-channel render buffer, which the mixing stage rejected. The console's conversion is unknown: the left and right channels are assumed to go to the first two buffer channels (front left and right) and the remaining channels stay silent, with no centre, LFE or surround content. Any other channel count mismatch still throws - [NGS2 voices without a format](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a sampler voice whose setup has no format (all zero) accepts an add of no waveform blocks without `APPEND`, which only applies `RESET` and the `CONTINUE` state, and matrix levels with no levels, which clear that matrix. PPSA36591 sets such a voice up, adds no blocks and sets no levels when a button is pressed at its title screen. Whether the console returns success or an error there is not measured; blocks with data, `APPEND` and an empty matrix on a voice with a format are still refused +- [NGS2 play without setup](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - not confirmed on the PS5 library: a `PLAY` event on a sampler voice that has no channels returns `0x804A8052` (`SCE_NGS2_ERROR_INVALID_NUM_CHANNELS` in the PS5 SDK) from Control and RunCommands and the voice stays idle (state flags 0), so the voice slots of PPSA03061 (Void Terrarium), which it frees by polling the state flags, are released. The PS4 firmware 11.00 `libSceNgs2.sprx` was measured through Control only (it returns `0x804A0052`) for a sampler voice patched to a mastering voice at 48000 Hz and grain 256, after a stop-immediately event and an add of no waveform blocks; the RunCommands result is inferred. Which voices count as set up (an all-zero format and a voice after a setup reset have no channels, see NGS2 voices without a format), what a failing list of parameters leaves applied before the failure and other racks were not measured; other racks keep their current behaviour - [NGS2 silent waveform blocks](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native): flag `0x10` (named `SILENCE` here; no public reference names it) lets a waveform block declare samples without data. PPSA04867 adds one block with flags `0x11`, `data_size` 0 and `num_samples` 16385, then another with flags `0x11` and a fully specified 0x800-byte, 1024-sample mono block, which takes the ordinary path. A dataless block under the flag outputs `num_samples` frames of silence, reports them through `decoded_data_size`, and finishes like a complete block; a dataless block without the flag still throws as not fitting its data. The flag's real name, its interaction with ATRAC9 and repeats, and the console's `waveform_data` for silent blocks are unverified on hardware - [NGS2 streaming PCM waveforms](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - PPSA21402 streams its music by adding one block whose `num_samples` is the whole track and whose `data_size` is a 16 KiB first piece, with `CONTINUE`, and then adds each following piece with flag `0x2` (called `APPEND` here) and the same `num_samples`. No reference describes flag `0x2`. A 16-bit PCM block without skipped samples or repeats whose `num_samples` exceed its data is assumed to start such a stream; flag `0x2` is assumed to append the block's data to the last streaming block, ignoring its `num_samples`; the voice is assumed to output silence without advancing while it has played all appended data and `CONTINUE` is set; and the waveform is assumed to end at `num_samples` or, once `CONTINUE` is cleared, at the end of the appended data. `sceNgs2VoiceGetState` reports the bytes played as `decoded_data_size`, which the title uses to release its queued data, and the read position as `waveform_data`. Appending to a block that is not a stream, or other formats, skipped samples, repeats and partial frames, throw - [NGS2 matrix levels](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - levels are clamped to -4..4 and NaN is kept, as the PS4 firmware 11.00 `libSceNgs2.sprx` stores them; not yet confirmed on the PS5 library. That library rejects a level count of 0 and keeps the matrix; the PS5 error code is unknown, so a count of 0 throws, except on a sampler voice without a format, where it clears the matrix (see NGS2 voices without a format). `sceNgs2VoiceRunCommands` is not in that library, so its matrix levels command is assumed to clamp the same way From 32cff4e0b33428cb157eeab756722b7910a4e210 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Fri, 23 Oct 2026 00:31:48 +0200 Subject: [PATCH 107/428] fix(libSceNgs2): return INVALID_VOICE_ACCESS for PLAY on a voice that was never set up, as the PS5 module does The PS5 libSceNgs2.native module (firmware 3.20 and 4.03) fails a play event on a voice with no setup with 0x804A8351 on every rack I tried and leaves it idle, so the guard now covers every rack and returns that code. Co-Authored-By: Claude Sonnet 5.5 --- .../libSceNgs2.native/include/Ngs2Types.hpp | 2 +- core/libs/prx/libSceNgs2.native/src/Voice.cpp | 2 +- .../tests/Ngs2VoiceEvents.cpp | 21 ++++++++++++------- docs/dev/debt/libSceNgs2.native.md | 2 +- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index c254121f1b..61e12fd2db 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -9,7 +9,7 @@ static constexpr int SCE_NGS2_OK = 0; static constexpr int SCE_NGS2_ERROR_INVALID_OUT_ADDRESS = static_cast(0x804A8010); static constexpr int SCE_NGS2_ERROR_INVALID_OUT_SIZE = static_cast(0x804A8011); -static constexpr int SCE_NGS2_ERROR_INVALID_NUM_CHANNELS = static_cast(0x804A8052); +static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_ACCESS = static_cast(0x804A8351); static constexpr int SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE = static_cast(0x804A8201); static constexpr int SCE_NGS2_ERROR_INVALID_RACK_HANDLE = static_cast(0x804A8202); static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_INDEX = static_cast(0x804A8305); diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index a1d2dafeff..b1b95cd204 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -25,7 +25,7 @@ struct Ngs2ReturnCode { }; static void SendEvent(Ngs2Voice& voice, std::uint32_t eventId) { - if (eventId == SCE_NGS2_VOICE_EVENT_PLAY && voice.rack->rackId == SCE_NGS2_RACK_ID_SAMPLER && voice.channels == 0) throw Ngs2ReturnCode{SCE_NGS2_ERROR_INVALID_NUM_CHANNELS}; + if (eventId == SCE_NGS2_VOICE_EVENT_PLAY && voice.channels == 0) throw Ngs2ReturnCode{SCE_NGS2_ERROR_INVALID_VOICE_ACCESS}; voice.SetEvent(eventId); } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceEvents.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceEvents.cpp index 5ad7626cd8..033ca97589 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceEvents.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceEvents.cpp @@ -23,8 +23,8 @@ static void TestPlayWithoutSetup() { const Ngs2VoiceCommand playCommand{2, 0, 4, 0, {.u = SCE_NGS2_VOICE_EVENT_PLAY}}; Event(voice, SCE_NGS2_VOICE_EVENT_STOP_IMM); Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, nullptr, 0, 0, nullptr}); - Require(sceNgs2VoiceControl(voice, &play.header) == SCE_NGS2_ERROR_INVALID_NUM_CHANNELS); - Require(sceNgs2VoiceRunCommands(voice, &playCommand, 1) == SCE_NGS2_ERROR_INVALID_NUM_CHANNELS); + Require(sceNgs2VoiceControl(voice, &play.header) == SCE_NGS2_ERROR_INVALID_VOICE_ACCESS); + Require(sceNgs2VoiceRunCommands(voice, &playCommand, 1) == SCE_NGS2_ERROR_INVALID_VOICE_ACCESS); std::vector out(Grain, -1.0f); const Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 1}; @@ -61,22 +61,27 @@ static void TestPlayAfterSetupInOneList() { Reversed reversed{PlayParam(), SamplerSetup}; reversed.play.header = {sizeof(reversed.play), static_cast(sizeof(reversed.play)), SCE_NGS2_VOICE_PARAM_EVENT}; reversed.setup.header = {sizeof(reversed.setup), 0, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP}; - Require(sceNgs2VoiceControl(other, &reversed.play.header) == SCE_NGS2_ERROR_INVALID_NUM_CHANNELS); + Require(sceNgs2VoiceControl(other, &reversed.play.header) == SCE_NGS2_ERROR_INVALID_VOICE_ACCESS); Require(Flags(other) == 0); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } -static void TestOtherRacksStillPlay() { +static void TestNeverSetUpVoicesOfOtherRacks() { const auto system = CreateSystem(); - const auto submixer = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SUBMIXER)); - Event(submixer, SCE_NGS2_VOICE_EVENT_PLAY); - Require(Flags(submixer) == SCE_NGS2_VOICE_STATE_FLAG_INUSE); + const auto play = PlayParam(); + for (const auto rackId : {SCE_NGS2_RACK_ID_SUBMIXER, SCE_NGS2_RACK_ID_REVERB, SCE_NGS2_RACK_ID_MASTERING}) { + const auto voice = Voice(CreateRack(system, rackId)); + Require(sceNgs2VoiceControl(voice, &play.header) == SCE_NGS2_ERROR_INVALID_VOICE_ACCESS); + Require(Flags(voice) == 0); + } + const auto master = Mastering(system, 1); + Require(Flags(master) == SCE_NGS2_VOICE_STATE_FLAG_INUSE); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } int main() { TestPlayWithoutSetup(); TestPlayAfterSetupInOneList(); - TestOtherRacksStillPlay(); + TestNeverSetUpVoicesOfOtherRacks(); return 0; } diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 6952c9a55c..b34c3ab758 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -14,7 +14,7 @@ - [NGS2 mastering voice gain](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - the param id 0x30000004 is taken from PPSA22520 and the layout from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2MasteringVoiceGainParam`; no reference gives the units or range. The levels are assumed to be linear factors applied when the mastering voice is mixed into the render buffer, so the voice's own samples stay unscaled; the LFE level applies to channel 3 of 6- and 8-channel voices, assuming the L R C LFE order of the AudioOut2 entries above, and the full-band level to the rest. Negative levels are accepted, and a new setup resets both levels to 1 - [NGS2 stereo mastering voice into a surround buffer](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - PPSA21402 renders a 2-channel mastering voice into a 6- or 8-channel render buffer, which the mixing stage rejected. The console's conversion is unknown: the left and right channels are assumed to go to the first two buffer channels (front left and right) and the remaining channels stay silent, with no centre, LFE or surround content. Any other channel count mismatch still throws - [NGS2 voices without a format](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a sampler voice whose setup has no format (all zero) accepts an add of no waveform blocks without `APPEND`, which only applies `RESET` and the `CONTINUE` state, and matrix levels with no levels, which clear that matrix. PPSA36591 sets such a voice up, adds no blocks and sets no levels when a button is pressed at its title screen. Whether the console returns success or an error there is not measured; blocks with data, `APPEND` and an empty matrix on a voice with a format are still refused -- [NGS2 play without setup](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - not confirmed on the PS5 library: a `PLAY` event on a sampler voice that has no channels returns `0x804A8052` (`SCE_NGS2_ERROR_INVALID_NUM_CHANNELS` in the PS5 SDK) from Control and RunCommands and the voice stays idle (state flags 0), so the voice slots of PPSA03061 (Void Terrarium), which it frees by polling the state flags, are released. The PS4 firmware 11.00 `libSceNgs2.sprx` was measured through Control only (it returns `0x804A0052`) for a sampler voice patched to a mastering voice at 48000 Hz and grain 256, after a stop-immediately event and an add of no waveform blocks; the RunCommands result is inferred. Which voices count as set up (an all-zero format and a voice after a setup reset have no channels, see NGS2 voices without a format), what a failing list of parameters leaves applied before the failure and other racks were not measured; other racks keep their current behaviour +- [NGS2 play without setup](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - differs from PS5 firmware 3.20 and 4.03 `libSceNgs2.native.sprx` in three cases: a set-up voice given an add of no waveform blocks and played keeps state flags 1 there for as long as it was watched and only until the next render here; an add of no blocks to a voice that already played and ended returns `0x804A8054` there and succeeds here; a playing voice reset by an all-zero setup keeps its state there and is stopped here. A `PLAY` on a voice with no channels returns `0x804A8351` (`SCE_NGS2_ERROR_INVALID_VOICE_ACCESS`) from Control and RunCommands, leaves it idle and skips the rest of the list, as that module does for every rack; PPSA03061 (Void Terrarium) frees its sound slots that way - [NGS2 silent waveform blocks](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native): flag `0x10` (named `SILENCE` here; no public reference names it) lets a waveform block declare samples without data. PPSA04867 adds one block with flags `0x11`, `data_size` 0 and `num_samples` 16385, then another with flags `0x11` and a fully specified 0x800-byte, 1024-sample mono block, which takes the ordinary path. A dataless block under the flag outputs `num_samples` frames of silence, reports them through `decoded_data_size`, and finishes like a complete block; a dataless block without the flag still throws as not fitting its data. The flag's real name, its interaction with ATRAC9 and repeats, and the console's `waveform_data` for silent blocks are unverified on hardware - [NGS2 streaming PCM waveforms](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - PPSA21402 streams its music by adding one block whose `num_samples` is the whole track and whose `data_size` is a 16 KiB first piece, with `CONTINUE`, and then adds each following piece with flag `0x2` (called `APPEND` here) and the same `num_samples`. No reference describes flag `0x2`. A 16-bit PCM block without skipped samples or repeats whose `num_samples` exceed its data is assumed to start such a stream; flag `0x2` is assumed to append the block's data to the last streaming block, ignoring its `num_samples`; the voice is assumed to output silence without advancing while it has played all appended data and `CONTINUE` is set; and the waveform is assumed to end at `num_samples` or, once `CONTINUE` is cleared, at the end of the appended data. `sceNgs2VoiceGetState` reports the bytes played as `decoded_data_size`, which the title uses to release its queued data, and the read position as `waveform_data`. Appending to a block that is not a stream, or other formats, skipped samples, repeats and partial frames, throw - [NGS2 matrix levels](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - levels are clamped to -4..4 and NaN is kept, as the PS4 firmware 11.00 `libSceNgs2.sprx` stores them; not yet confirmed on the PS5 library. That library rejects a level count of 0 and keeps the matrix; the PS5 error code is unknown, so a count of 0 throws, except on a sampler voice without a format, where it clears the matrix (see NGS2 voices without a format). `sceNgs2VoiceRunCommands` is not in that library, so its matrix levels command is assumed to clamp the same way From ea69a7a2e36a454fe32ae3ec782998f409e7e80d Mon Sep 17 00:00:00 2001 From: hypermist Date: Fri, 23 Oct 2026 13:40:40 +1300 Subject: [PATCH 108/428] fix(agc): ask for at least three swapchain images instead of one extra Surfaces that already report three images keep three, so FIFO swapchains on X11 and Mesa Wayland get no fourth image. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SNqSvnFwRBz3D76n476yw6 --- .../libSceAgcDriver/Execution/include/SwapchainImages.hpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Flip.cpp | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/SwapchainImages.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/SwapchainImages.hpp index 032b5fc86c..b3411ca19b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/SwapchainImages.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/SwapchainImages.hpp @@ -8,7 +8,7 @@ namespace AgcDriver { inline std::uint32_t SwapchainImageCount(const VkSurfaceCapabilitiesKHR& surface) { - const std::uint32_t count = surface.minImageCount + 1; + const std::uint32_t count = std::max(surface.minImageCount, 3u); return surface.maxImageCount != 0 ? std::min(count, surface.maxImageCount) : count; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp index 95c49020b9..27e913cd57 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp @@ -408,10 +408,12 @@ void testPresentMode() { surface.maxImageCount = 8; check(AgcDriver::SwapchainImageCount(surface) == 3, "the swapchain has no image beyond the surface minimum"); surface.minImageCount = 3; - check(AgcDriver::SwapchainImageCount(surface) == 4, "the swapchain does not follow a larger surface minimum"); - surface.maxImageCount = 3; - check(AgcDriver::SwapchainImageCount(surface) == 3, "the swapchain exceeds the surface maximum"); + check(AgcDriver::SwapchainImageCount(surface) == 3, "a three-image surface minimum gained an image"); + surface.minImageCount = 5; + check(AgcDriver::SwapchainImageCount(surface) == 5, "the swapchain does not follow a larger surface minimum"); surface.minImageCount = 2; + surface.maxImageCount = 2; + check(AgcDriver::SwapchainImageCount(surface) == 2, "the swapchain exceeds the surface maximum"); surface.maxImageCount = 0; check(AgcDriver::SwapchainImageCount(surface) == 3, "an unlimited surface maximum capped the swapchain"); const std::array wayland{VK_PRESENT_MODE_FIFO_KHR, VK_PRESENT_MODE_IMMEDIATE_KHR}; From b2a6d79a890468e04370b05ff8b631000c0eb52f Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Thu, 22 Oct 2026 05:59:40 +0200 Subject: [PATCH 109/428] docs(debt): record what a Windows datagram socket keeps after sceNetConnect to AF_UNSPEC sceNetConnect dissolves a datagram association with the same host call sequence as connect (disconnect_datagram in Export.cpp). The sceNetConnect-to-AF_UNSPEC debt entry only described Linux. On Windows a socket bound to an explicit address keeps it in the same way, and a sendto on the host socket to a non-loopback address fails with Winsock error 10051, where FreeBSD's udp_disconnect() resets the local address. Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceNet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index f9f697287e..c6a926bcc2 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -30,7 +30,7 @@ - [sceNetResolverStartAton6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reverse-resolves a 16-byte IPv6 address through the host resolver (`getnameinfo` with `NI_NAMEREQD`) into the caller's buffer of `len` bytes, as `sceNetResolverStartAton` does for IPv4. The `(rid, addr, hostname, len, timeout, retry, flags)` signature comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L992-L997). An address with no name, or a name longer than `len - 1` bytes, fails with `RESOLVER_ENODNS` (`0x804101E1`) and leaves the buffer untouched; whether the console truncates instead is not verified. Abort flag 2 (ATON preservation) fails the next call with `EINTR` as it does `sceNetResolverStartAton`. Timeout and retry are ignored, and any nonzero flags throw because their meaning is unknown - [sceNetAccept](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) inherits the listener's non-blocking mode through the existing `SO_NBIO` setter, as the libkernel socket path does and FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L571) copies the socket state. The mode is sampled when accept starts; changes between connection creation and accept are not modelled. Not measured on a console. -- [sceNetConnect to AF_UNSPEC](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had +- [sceNetConnect to AF_UNSPEC](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux and on Windows a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again, and `sceNetGetsockname` reports that host address. On Windows a `sendto` on such a host socket to a non-loopback address fails with Winsock error 10051 (measured on the host socket, not through `sceNetSendto`), where FreeBSD sends it from the wildcard address. One bound to port 0 is bound again to the wildcard address with the port it had - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. Windows sockets stay non-blocking and the library waits for readiness until the stored timeout. Blocking stream sends return the queued byte count on timeout, or `EAGAIN` when no bytes were queued. - [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) take `SO_SNDTIMEO` and `SO_RCVTIMEO` (0x1005, 0x1006) as a 16-byte `struct timeval`, as FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/uipc_socket.c#L2657-L2690) do, and Sony's 0x1105 and 0x1106 as an `int` of microseconds. A [console log of ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/issues/49) (firmware 12.20) shows receives timing out every 100 ms after it gave 0x1006 a `{0, 100000}` timeval. The errors for a value shorter than a timeval (`EINVAL`) or out of range (`EDOM`), and `sceNetGetsockopt` cutting the timeval to the caller's length, follow FreeBSD's `sooptcopyin` and `sooptcopyout` and are not measured on a console - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) stores `SO_ONESBCAST` (`0x10000`, [shadPS4's `ORBIS_NET_SO_ONESBCAST`](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net_types.h)) per socket and `sceNetGetsockopt` reads back the value that was set, as shadPS4 does; PPSA34501 sets it and crashes later when it fails with `EOPNOTSUPP`. Sends ignore it. FreeBSD 11.4's [ip(4)](https://man.freebsd.org/cgi/man.cgi?manpath=FreeBSD+11.4-RELEASE&query=ip&sektion=4) documents the like-named `IPPROTO_IP` option `IP_ONESBCAST`: a datagram sent to an interface's directed broadcast address, such as 192.168.2.255, leaves through that interface with its destination rewritten to 255.255.255.255, and only on a socket allowed to set `SO_BROADCAST`. Here host routing selects the outgoing interface and the destination is not rewritten. shadPS4 also leaves sends unchanged, and its [TODO](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp#L2467-L2470) reads the option the other way round: a send to 255.255.255.255 would be converted to the interface's broadcast address. Whether the console's `SOL_SOCKET` option behaves like `IP_ONESBCAST`, and what it reads back, is not verified on hardware. FreeBSD 11.4's `sogetopt()` reads several boolean socket options back as their flag bit (`so_options & optname`, `sys/kern/uipc_socket.c`), not as the value set; whether the console does so for `0x10000` is unknown From 59693ae54ca6f169b69f21227856df2a6fd4f2c8 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 21 Oct 2026 22:46:23 -0400 Subject: [PATCH 110/428] feat(tools): symbolize the Windows samplers' stacks The Windows samplers (APS5_SAMPLE_WORKER, APS5_SAMPLE_COMMITTER and APS5_SAMPLE_THREADS) write every frame as module+0xRVA, and BUILD.md left naming them to the reader ("for example with nm on the .prx"). Doing that by hand means finding each module's image base, choosing between COFF symbols and the export table, and keeping the files cumulative totals in mind. tools/symbolize_samples.py reads a collapsed-stack file or the address file, opens each module it finds in --libs as a PE32+ image, and names every frame module!function: - From the COFF symbol table that the MinGW-built .prx and .dll keep: external and static symbols in code sections, at the section's RVA plus the symbol value. Addresses outside a code section, or before the first symbol, stay module+0xRVA. - From the export table, prefixed with "~", for a module without symbols, such as the runtime DLLs. - A module whose SizeOfImage or TimeDateStamp differs from the sampler's "# module" line is left unsymbolized, with a warning: its addresses would name the wrong functions. --since subtracts an earlier copy of the same file, which keeps only the samples taken between the two copies (the files hold totals since start-up). --out writes the named stacks for speedscope or flamegraph.pl. --demangle uses c++filt when it is on PATH. It needs no binutils otherwise. test_symbolize_samples (registered with CTest) builds small PE32+ images with COFF symbols and with an export table, and covers short, string-table and static names, the ignored section and data symbols, addresses outside code, the export fallback, the size and timestamp check, --since with --out, the address-file format and rejected lines. Co-Authored-By: Claude Opus 5.5 --- CMakeLists.txt | 2 + docs/dev/BUILD.md | 4 +- tools/symbolize_samples.py | 260 ++++++++++++++++++++++++++ tools/tests/test_symbolize_samples.py | 151 +++++++++++++++ 4 files changed, 415 insertions(+), 2 deletions(-) create mode 100644 tools/symbolize_samples.py create mode 100644 tools/tests/test_symbolize_samples.py diff --git a/CMakeLists.txt b/CMakeLists.txt index b4bb291af5..f66df6f627 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -240,6 +240,8 @@ if(BUILD_TESTING) set_tests_properties(pad_timeline PROPERTIES TIMEOUT 20) add_test(NAME trace_digest COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_trace_digest.py) + add_test(NAME symbolize_samples COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_symbolize_samples.py) add_test(NAME conventions_bom_shebang COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_bom_shebang.py) set_tests_properties(conventions_bom_shebang PROPERTIES TIMEOUT 20) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 7778788a16..61cb860139 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -196,11 +196,11 @@ On a device that supports `VK_EXT_device_fault`, the first `VK_ERROR_DEVICE_LOST ## Worker sampler -Set `APS5_SAMPLE_WORKER=` to sample queue worker 0 every millisecond (in practice every timer tick). Every 20 s and at exit it writes ``, one `module offset self inclusive` line per sampled address, and `.collapsed.txt`, the worker's whole stacks in the collapsed format that speedscope.app and flamegraph.pl read: one `frame;...;frame count` line per distinct stack, root first, each frame as `module+0xRVA`. A walk that ends before the thread's root starts with `[truncated]`. The `#` lines give the samples, the walk depth (64 frames), the truncated walks, the mean and longest time the worker was suspended, and the size and PE timestamp of each module, to check the binary a file is symbolized against. A frame with `.pdata` is stepped by `RtlVirtualUnwind`; any other frame by its DWARF record in `.ehfram`, the unwind data the libraries are built with. Those steps read only inside the worker's stack, and a module is indexed after the worker resumes, the first time a walk meets it. +Set `APS5_SAMPLE_WORKER=` to sample queue worker 0 every millisecond (in practice every timer tick). Every 20 s and at exit it writes ``, one `module offset self inclusive` line per sampled address, and `.collapsed.txt`, the worker's whole stacks in the collapsed format that speedscope.app and flamegraph.pl read: one `frame;...;frame count` line per distinct stack, root first, each frame as `module+0xRVA`. A walk that ends before the thread's root starts with `[truncated]`. The `#` lines give the samples, the walk depth (64 frames), the truncated walks, the mean and longest time the worker was suspended, and the size and PE timestamp of each module, to check the binary a file is symbolized against. A frame with `.pdata` is stepped by `RtlVirtualUnwind`; any other frame by its DWARF record in `.ehfram`, the unwind data the libraries are built with. Those steps read only inside the worker's stack, and a module is indexed after the worker resumes, the first time a walk meets it. `python tools/symbolize_samples.py .collapsed.txt --libs ` names each frame from the COFF symbols of the module that ran, or from its export table when it has none, and leaves a module unsymbolized when its size or timestamp differs from the `# module` line; `--since ` keeps only the samples taken after that copy, `--out` writes the named stacks and `--demangle` uses `c++filt`. `APS5_SAMPLE_COMMITTER=` samples queue 0's draw committer the same way and writes the same two files. The committer records the draws the worker hands over (`APS5_PIPELINED_DRAWS`), so when the worker's stacks wait in `DrawPipeline::Enqueue`, its stacks show what bounds the frame. It starts with the first pipelined draw; with `APS5_PIPELINED_DRAWS=0` nothing is sampled. -`APS5_SAMPLE_THREADS=` samples every thread of the process every 2 ms and writes the 40 hottest addresses of each busy thread to ``, 12 frames deep and through `.pdata` only. The samplers exist only on Windows; the addresses are symbolized offline, for example with `nm` on the `.prx`. +`APS5_SAMPLE_THREADS=` samples every thread of the process every 2 ms and writes the 40 hottest addresses of each busy thread to ``, 12 frames deep and through `.pdata` only. The samplers exist only on Windows; the addresses are symbolized offline with `tools/symbolize_samples.py`. ## Performance trace diff --git a/tools/symbolize_samples.py b/tools/symbolize_samples.py new file mode 100644 index 0000000000..e0254f0fa4 --- /dev/null +++ b/tools/symbolize_samples.py @@ -0,0 +1,260 @@ +import argparse +import bisect +import collections +import shutil +import struct +import subprocess +import sys +from pathlib import Path + +DESCRIPTION = """Symbolize the output of the Windows samplers (APS5_SAMPLE_WORKER, APS5_SAMPLE_COMMITTER and +APS5_SAMPLE_THREADS) against the modules that ran. Each sampled frame is written as module+0xRVA; this reads the +COFF symbol table of each module found in --libs (the MinGW-built .prx and .dll files keep one), or its export +table when it has none, and names every frame module!function. + +The '# module name size timestamp' header lines the samplers write are compared with the SizeOfImage and +TimeDateStamp of the file that is found; a module whose file does not match is left unsymbolized, since its +addresses would name the wrong functions. + +The sampler rewrites its files every 20 s with totals since start-up. --since takes an earlier copy of the same +file and subtracts it, which keeps only the samples taken between the two copies (a steady-state window). + +Input: a collapsed-stack file (.collapsed.txt, 'frame;...;frame count' lines) or the address file +('module offset self inclusive' lines). Output: the hottest functions by self and by inclusive samples, and with +--out a symbolized collapsed-stack file for speedscope.app or flamegraph.pl.""" + +IMAGE_SCN_CNT_CODE = 0x20 +IMAGE_SYM_CLASS_EXTERNAL = 2 +IMAGE_SYM_CLASS_STATIC = 3 + + +class PeModule: + def __init__(self, path): + data = Path(path).read_bytes() + if data[:2] != b"MZ": + raise ValueError(f"{path}: not a PE file") + pe = struct.unpack_from("> 4) != 2 and value == 0: + continue + if name[:4] == b"\0\0\0\0": + at = strings + struct.unpack_from(" 0}) + + +def summarize(symbolizer, stacks, addresses, top): + self_counts = collections.Counter() + inclusive = collections.Counter() + total = 0 + if stacks: + for frames, count in stacks.items(): + names = [symbolizer.frame(frame) for frame in frames.split(";")] + total += count + self_counts[names[-1]] += count + for name in set(names): + inclusive[name] += count + else: + for (module, offset), count in addresses.items(): + total += count + self_counts[symbolizer.frame(f"{module}+{offset}")] += count + lines = [f"{total} samples"] + for title, counter in (("self", self_counts), ("inclusive", inclusive)): + if not counter: + continue + lines.append(f"hottest by {title}:") + for name, count in counter.most_common(top): + lines.append(f" {count:8d} {100.0 * count / total if total else 0.0:5.1f}% {name}") + return lines + + +def main(argv=None): + parser = argparse.ArgumentParser(description=DESCRIPTION, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("samples", help="collapsed-stack file or address file written by a Windows sampler") + parser.add_argument("--libs", action="append", required=True, help="directory holding the modules that ran (repeatable)") + parser.add_argument("--since", help="an earlier copy of the same file, subtracted to keep the samples taken after it") + parser.add_argument("--out", help="write the symbolized collapsed stacks to this file") + parser.add_argument("--top", type=int, default=30, help="functions listed per table (default 30)") + parser.add_argument("--demangle", action="store_true", help="demangle C++ names with c++filt when it is on PATH") + args = parser.parse_args(argv) + headers, stacks, addresses = load(args.samples) + if args.since: + earlier_headers, earlier_stacks, earlier_addresses = load(args.since) + stacks = subtract(stacks, earlier_stacks) + addresses = subtract(addresses, earlier_addresses) + headers = {**earlier_headers, **headers} + symbolizer = Symbolizer(args.libs, headers, args.demangle, warn=lambda text: print("warning: " + text, file=sys.stderr)) + for line in summarize(symbolizer, stacks, addresses, args.top): + print(line) + if args.out: + if not stacks: + parser.error("--out needs a collapsed-stack file") + with open(args.out, "w", encoding="utf-8") as output: + for frames, count in stacks.items(): + output.write(";".join(symbolizer.frame(frame) for frame in frames.split(";")) + f" {count}\n") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/tests/test_symbolize_samples.py b/tools/tests/test_symbolize_samples.py new file mode 100644 index 0000000000..c5c1c15a29 --- /dev/null +++ b/tools/tests/test_symbolize_samples.py @@ -0,0 +1,151 @@ +import contextlib +import io +import struct +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import symbolize_samples + +TIMESTAMP = 0x6AD92AC4 +SIZE_OF_IMAGE = 0x3000 + + +def symbol(name, value, section, kind, storage, aux=0): + return struct.pack("<8sIhHBB", name, value, section, kind, storage, aux) + + +def build_pe(path, with_symbols=True, with_exports=False): + image = bytearray(0xC00) + image[0:2] = b"MZ" + struct.pack_into(" Date: Thu, 22 Oct 2026 00:36:07 -0400 Subject: [PATCH 111/428] fix(tools): bound symbol lookups to their section, keep inclusive counts and refuse unrecorded modules Review of #9572 found three gaps: - PeModule.name() checked that an RVA was in some code section, then bisected one list of every symbol, so an address in a later code section before that section's first symbol took the last symbol of an earlier section. Each code section now keeps its own sorted list. - Address files keep both counters ('module offset self inclusive'): the inclusive count is kept and subtracted by --since, and the summary lists functions by inclusive samples as well, summed over each function's addresses (an upper bound when a function is on one stack more than once). The total comes from the '# N samples' line or from the 'thread samples ' headings of APS5_SAMPLE_THREADS, which the loader previously misread as a stack line; --thread keeps one thread. - The address writers of both samplers wrote no '# module' records, so those files skipped the size and timestamp check. Both now write them (through the helper the collapsed-stack writer uses), and the tool leaves a module unsymbolized when the samples have no record for it, unless --unverified is given. Five new cases cover a second code section, inclusive totals, --since on an address file, thread headings with --thread, and the missing record with and without --unverified. Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/WorkerSampler.cpp | 20 +++- docs/dev/BUILD.md | 4 +- tools/symbolize_samples.py | 110 ++++++++++++------ tools/tests/test_symbolize_samples.py | 73 ++++++++++-- 4 files changed, 154 insertions(+), 53 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp index 7bbca06017..82a909461c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp @@ -162,18 +162,29 @@ struct Sampler { return frame; } + static void writeModules(std::FILE* file, const std::map& loaded) { + for (const auto& [base, name] : loaded) { + const auto* dos = reinterpret_cast(base); + const auto* nt = reinterpret_cast(base + dos->e_lfanew); + std::fprintf(file, "# module %s 0x%lx 0x%lx\n", name.c_str(), static_cast(nt->OptionalHeader.SizeOfImage), static_cast(nt->FileHeader.TimeDateStamp)); + } + } + void write() { std::lock_guard lock(mutex); std::FILE* file = std::fopen(path.c_str(), "w"); if (file == nullptr) return; std::fprintf(file, "# %llu samples\n", static_cast(samples)); + std::map loaded; for (const auto& [address, count] : counts) { HMODULE module = nullptr; char name[MAX_PATH] = "?"; if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(address), &module)) GetModuleFileNameA(module, name, sizeof(name)); const char* base = std::strrchr(name, '\\'); + if (module != nullptr) loaded.emplace(reinterpret_cast(module), base ? base + 1 : name); std::fprintf(file, "%s 0x%llx %llu %llu\n", base ? base + 1 : name, static_cast(address - reinterpret_cast(module)), static_cast(count.first), static_cast(count.second)); } + writeModules(file, loaded); std::fclose(file); if (keepStacks) writeStacks(); } @@ -194,11 +205,7 @@ struct Sampler { const double tickUs = 1e6 / static_cast(frequency.QuadPart); std::fprintf(file, "# samples=%llu depth=%d truncated=%llu suspended_us_mean=%.1f suspended_us_max=%.1f\n", static_cast(samples), depth, static_cast(stacks.Truncated()), samples != 0 ? static_cast(suspendedTicks) * tickUs / static_cast(samples) : 0.0, static_cast(longestSuspendedTicks) * tickUs); - for (const auto& [base, name] : loaded) { - const auto* dos = reinterpret_cast(base); - const auto* nt = reinterpret_cast(base + dos->e_lfanew); - std::fprintf(file, "# module %s 0x%lx 0x%lx\n", name.c_str(), static_cast(nt->OptionalHeader.SizeOfImage), static_cast(nt->FileHeader.TimeDateStamp)); - } + writeModules(file, loaded); const auto text = stacks.Format([&](std::uint64_t address) { return names.at(address); }); const bool written = std::fwrite(text.data(), 1, text.size(), file) == text.size(); if (std::fclose(file) != 0 || !written) return; @@ -245,6 +252,7 @@ struct ProcessSampler { std::FILE* file = std::fopen(path.c_str(), "w"); if (file == nullptr) return; std::fprintf(file, "# %llu rounds\n", static_cast(rounds)); + std::map loaded; for (auto& [id, thread] : threads) { std::lock_guard lock(thread.mutex); std::vector>> hot(thread.counts.begin(), thread.counts.end()); @@ -259,9 +267,11 @@ struct ProcessSampler { char name[MAX_PATH] = "?"; if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(address), &module)) GetModuleFileNameA(module, name, sizeof(name)); const char* base = std::strrchr(name, '\\'); + if (module != nullptr) loaded.emplace(reinterpret_cast(module), base ? base + 1 : name); std::fprintf(file, " %s 0x%llx %llu %llu\n", base ? base + 1 : name, static_cast(address - reinterpret_cast(module)), static_cast(hot[i].second.first), static_cast(hot[i].second.second)); } } + Sampler::writeModules(file, loaded); std::fclose(file); } }; diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 61cb860139..63390e032d 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -196,11 +196,11 @@ On a device that supports `VK_EXT_device_fault`, the first `VK_ERROR_DEVICE_LOST ## Worker sampler -Set `APS5_SAMPLE_WORKER=` to sample queue worker 0 every millisecond (in practice every timer tick). Every 20 s and at exit it writes ``, one `module offset self inclusive` line per sampled address, and `.collapsed.txt`, the worker's whole stacks in the collapsed format that speedscope.app and flamegraph.pl read: one `frame;...;frame count` line per distinct stack, root first, each frame as `module+0xRVA`. A walk that ends before the thread's root starts with `[truncated]`. The `#` lines give the samples, the walk depth (64 frames), the truncated walks, the mean and longest time the worker was suspended, and the size and PE timestamp of each module, to check the binary a file is symbolized against. A frame with `.pdata` is stepped by `RtlVirtualUnwind`; any other frame by its DWARF record in `.ehfram`, the unwind data the libraries are built with. Those steps read only inside the worker's stack, and a module is indexed after the worker resumes, the first time a walk meets it. `python tools/symbolize_samples.py .collapsed.txt --libs ` names each frame from the COFF symbols of the module that ran, or from its export table when it has none, and leaves a module unsymbolized when its size or timestamp differs from the `# module` line; `--since ` keeps only the samples taken after that copy, `--out` writes the named stacks and `--demangle` uses `c++filt`. +Set `APS5_SAMPLE_WORKER=` to sample queue worker 0 every millisecond (in practice every timer tick). Every 20 s and at exit it writes ``, one `module offset self inclusive` line per sampled address, and `.collapsed.txt`, the worker's whole stacks in the collapsed format that speedscope.app and flamegraph.pl read: one `frame;...;frame count` line per distinct stack, root first, each frame as `module+0xRVA`. A walk that ends before the thread's root starts with `[truncated]`. The `#` lines give the samples, the walk depth (64 frames), the truncated walks, the mean and longest time the worker was suspended, and the size and PE timestamp of each module, to check the binary a file is symbolized against. A frame with `.pdata` is stepped by `RtlVirtualUnwind`; any other frame by its DWARF record in `.ehfram`, the unwind data the libraries are built with. Those steps read only inside the worker's stack, and a module is indexed after the worker resumes, the first time a walk meets it. `python tools/symbolize_samples.py .collapsed.txt --libs ` names each frame from the COFF symbols of the module that ran, or from its export table when it has none, and leaves a module unsymbolized when its size or timestamp differs from its `# module` line or when it has none (`--unverified` symbolizes it anyway); `--since ` keeps only the samples taken after that copy, `--out` writes the named stacks and `--demangle` uses `c++filt`. Given ``, it lists functions by self and by inclusive samples, where a function's inclusive count is the sum of its addresses' and overcounts a function that is on one stack more than once. `APS5_SAMPLE_COMMITTER=` samples queue 0's draw committer the same way and writes the same two files. The committer records the draws the worker hands over (`APS5_PIPELINED_DRAWS`), so when the worker's stacks wait in `DrawPipeline::Enqueue`, its stacks show what bounds the frame. It starts with the first pipelined draw; with `APS5_PIPELINED_DRAWS=0` nothing is sampled. -`APS5_SAMPLE_THREADS=` samples every thread of the process every 2 ms and writes the 40 hottest addresses of each busy thread to ``, 12 frames deep and through `.pdata` only. The samplers exist only on Windows; the addresses are symbolized offline with `tools/symbolize_samples.py`. +`APS5_SAMPLE_THREADS=` samples every thread of the process every 2 ms and writes the 40 hottest addresses of each busy thread to ``, 12 frames deep and through `.pdata` only, under a `thread samples ` line per thread and with the same `# module` lines. The samplers exist only on Windows; the addresses are symbolized offline with `tools/symbolize_samples.py`, which adds up all threads or, with `--thread `, keeps one. ## Performance trace diff --git a/tools/symbolize_samples.py b/tools/symbolize_samples.py index e0254f0fa4..f61b586e53 100644 --- a/tools/symbolize_samples.py +++ b/tools/symbolize_samples.py @@ -19,9 +19,14 @@ addresses would name the wrong functions. The sampler rewrites its files every 20 s with totals since start-up. --since takes an earlier copy of the same file and subtracts it, which keeps only the samples taken between the two copies (a steady-state window). -Input: a collapsed-stack file (.collapsed.txt, 'frame;...;frame count' lines) or the address file -('module offset self inclusive' lines). Output: the hottest functions by self and by inclusive samples, and with ---out a symbolized collapsed-stack file for speedscope.app or flamegraph.pl.""" +A module with no such record is also left unsymbolized, since nothing shows which build it was; --unverified +symbolizes it anyway against the file found. + +Input: a collapsed-stack file (.collapsed.txt, 'frame;...;frame count' lines) or an address file +('module offset self inclusive' lines, under 'thread samples ' headings for APS5_SAMPLE_THREADS; --thread +keeps one thread). Output: the hottest functions by self and by inclusive samples, and with --out a symbolized +collapsed-stack file for speedscope.app or flamegraph.pl. For an address file, a function's inclusive count is the +sum of its addresses' counts, which overcounts the samples in which it is on the stack more than once.""" IMAGE_SCN_CNT_CODE = 0x20 IMAGE_SYM_CLASS_EXTERNAL = 2 @@ -54,8 +59,12 @@ class PeModule: if self.exported and export_rva: symbols = self.export_symbols(data, export_rva, export_size) symbols.sort() - self.code = [(start, start + size) for start, size, _, characteristics in self.sections if characteristics & IMAGE_SCN_CNT_CODE] - self.addresses = [address for address, _ in symbols] + self.code = [] + for start, size, _, characteristics in self.sections: + if not characteristics & IMAGE_SCN_CNT_CODE: + continue + inside = [(address, name) for address, name in symbols if start <= address < start + size] + self.code.append((start, start + size, [address for address, _ in inside], [name for _, name in inside])) self.names = [name for _, name in symbols] def offset(self, rva): @@ -106,16 +115,15 @@ class PeModule: return result def name(self, rva): - if not any(start <= rva < end for start, end in self.code): - return None - index = bisect.bisect_right(self.addresses, rva) - 1 - if index < 0: - return None - return ("~" if self.exported else "") + self.names[index] + for start, end, addresses, names in self.code: + if start <= rva < end: + index = bisect.bisect_right(addresses, rva) - 1 + return None if index < 0 else ("~" if self.exported else "") + names[index] + return None class Symbolizer: - def __init__(self, libs, headers, demangle=False, warn=print): + def __init__(self, libs, headers, demangle=False, warn=print, unverified=False): self.files = {} for directory in libs: for path in Path(directory).iterdir(): @@ -126,6 +134,7 @@ class Symbolizer: self.demangle = demangle and shutil.which("c++filt") is not None self.demangled = {} self.warn = warn + self.unverified = unverified def module(self, name): if name in self.modules: @@ -137,6 +146,9 @@ class Symbolizer: module = PeModule(path) except (ValueError, struct.error) as error: self.warn(f"{name}: {error}") + if module is not None and name not in self.headers and not self.unverified: + self.warn(f"{name}: the samples have no '# module' record for it, so {path} may be another build; its frames stay unsymbolized (--unverified symbolizes them anyway)") + module = None if module is not None and name in self.headers and self.headers[name] != (module.size, module.timestamp): size, timestamp = self.headers[name] self.warn(f"{name}: the sampled module had size {size:#x} and timestamp {timestamp:#x}, but {path} has {module.size:#x} and {module.timestamp:#x}; its frames stay unsymbolized") @@ -179,23 +191,42 @@ def read_headers(lines): return headers -def load(path): +class Samples: + def __init__(self): + self.headers = {} + self.stacks = collections.Counter() + self.self_counts = collections.Counter() + self.inclusive = collections.Counter() + self.total = collections.Counter() + + +def load(path, thread=None): lines = Path(path).read_text(encoding="utf-8-sig").splitlines() - headers = read_headers(lines) - stacks = collections.Counter() - addresses = collections.Counter() + samples = Samples() + samples.headers = read_headers(lines) + current = None for number, line in enumerate(lines, 1): + parts = line.split() + if len(parts) == 3 and parts[0] == "#" and parts[2] == "samples" and parts[1].isdigit(): + samples.total["samples"] += int(parts[1]) + continue if line.startswith("#") or not line.strip(): continue - parts = line.split() + if len(parts) == 4 and parts[0] == "thread" and parts[2] == "samples" and parts[1].isdigit() and parts[3].isdigit(): + current = parts[1] + if thread is None or current == thread: + samples.total["threads"] += int(parts[3]) + continue if len(parts) == 4 and parts[1].startswith("0x") and parts[2].isdigit() and parts[3].isdigit(): - addresses[(parts[0], parts[1])] += int(parts[2]) + if thread is None or current == thread: + samples.self_counts[(parts[0], parts[1])] += int(parts[2]) + samples.inclusive[(parts[0], parts[1])] += int(parts[3]) continue frames, _, count = line.rpartition(" ") if not frames or not count.isdigit(): - raise ValueError(f"{path}:{number}: expected 'frame;...;frame count' or 'module offset self inclusive'") - stacks[frames] += int(count) - return headers, stacks, addresses + raise ValueError(f"{path}:{number}: expected 'frame;...;frame count', 'module offset self inclusive' or 'thread samples '") + samples.stacks[frames] += int(count) + return samples def subtract(later, earlier): @@ -204,21 +235,31 @@ def subtract(later, earlier): return collections.Counter({key: count for key, count in result.items() if count > 0}) -def summarize(symbolizer, stacks, addresses, top): +def window(later, earlier): + result = Samples() + result.headers = {**earlier.headers, **later.headers} + for name in ("stacks", "self_counts", "inclusive", "total"): + setattr(result, name, subtract(getattr(later, name), getattr(earlier, name))) + return result + + +def summarize(symbolizer, samples, top): self_counts = collections.Counter() inclusive = collections.Counter() total = 0 - if stacks: - for frames, count in stacks.items(): + if samples.stacks: + for frames, count in samples.stacks.items(): names = [symbolizer.frame(frame) for frame in frames.split(";")] total += count self_counts[names[-1]] += count for name in set(names): inclusive[name] += count else: - for (module, offset), count in addresses.items(): - total += count + for (module, offset), count in samples.self_counts.items(): self_counts[symbolizer.frame(f"{module}+{offset}")] += count + for (module, offset), count in samples.inclusive.items(): + inclusive[symbolizer.frame(f"{module}+{offset}")] += count + total = samples.total["threads"] or samples.total["samples"] or sum(samples.self_counts.values()) lines = [f"{total} samples"] for title, counter in (("self", self_counts), ("inclusive", inclusive)): if not counter: @@ -237,21 +278,20 @@ def main(argv=None): parser.add_argument("--out", help="write the symbolized collapsed stacks to this file") parser.add_argument("--top", type=int, default=30, help="functions listed per table (default 30)") parser.add_argument("--demangle", action="store_true", help="demangle C++ names with c++filt when it is on PATH") + parser.add_argument("--thread", help="keep only this thread of an APS5_SAMPLE_THREADS file") + parser.add_argument("--unverified", action="store_true", help="also symbolize modules the samples have no '# module' record for") args = parser.parse_args(argv) - headers, stacks, addresses = load(args.samples) + samples = load(args.samples, args.thread) if args.since: - earlier_headers, earlier_stacks, earlier_addresses = load(args.since) - stacks = subtract(stacks, earlier_stacks) - addresses = subtract(addresses, earlier_addresses) - headers = {**earlier_headers, **headers} - symbolizer = Symbolizer(args.libs, headers, args.demangle, warn=lambda text: print("warning: " + text, file=sys.stderr)) - for line in summarize(symbolizer, stacks, addresses, args.top): + samples = window(samples, load(args.since, args.thread)) + symbolizer = Symbolizer(args.libs, samples.headers, args.demangle, warn=lambda text: print("warning: " + text, file=sys.stderr), unverified=args.unverified) + for line in summarize(symbolizer, samples, args.top): print(line) if args.out: - if not stacks: + if not samples.stacks: parser.error("--out needs a collapsed-stack file") with open(args.out, "w", encoding="utf-8") as output: - for frames, count in stacks.items(): + for frames, count in samples.stacks.items(): output.write(";".join(symbolizer.frame(frame) for frame in frames.split(";")) + f" {count}\n") return 0 diff --git a/tools/tests/test_symbolize_samples.py b/tools/tests/test_symbolize_samples.py index c5c1c15a29..5e000527dc 100644 --- a/tools/tests/test_symbolize_samples.py +++ b/tools/tests/test_symbolize_samples.py @@ -17,7 +17,7 @@ def symbol(name, value, section, kind, storage, aux=0): return struct.pack("<8sIhHBB", name, value, section, kind, storage, aux) -def build_pe(path, with_symbols=True, with_exports=False): +def build_pe(path, with_symbols=True, with_exports=False, second_code=False): image = bytearray(0xC00) image[0:2] = b"MZ" struct.pack_into(" Date: Thu, 22 Oct 2026 10:47:52 +0700 Subject: [PATCH 112/428] perf(agc): write the frame-dump log line through the asynchronous profile output Co-Authored-By: Claude --- core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 9d93a5127f..bbbb45fa6d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -720,7 +720,7 @@ struct VulkanDevice::State { for (std::uint32_t x = 0; x < width; ++x) std::memcpy(pixels.data() + (static_cast(y) * width + x) * 4, full.data() + (static_cast(y) * scale * slot.dumpWidth + static_cast(x) * scale) * 4, 4); } 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, slot.dumpWidth, slot.dumpHeight, std::chrono::duration(std::chrono::steady_clock::now() - start).count()); + AgcDriver::ProfilePrint_nid_no_patch("[gpu] frame_%03d.bmp: %ux%u display read back on the GPU at %.1f s\n", index, slot.dumpWidth, slot.dumpHeight, std::chrono::duration(std::chrono::steady_clock::now() - start).count()); dumpWriter.Enqueue(index, width, height, 1, false, std::move(pixels)); } From 0ec6032d238ac7d828371408060a67bd57f18db9 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Thu, 22 Oct 2026 13:28:06 +0700 Subject: [PATCH 113/428] fix(agc): store resident linear color targets at completion points, limited to their drawn rectangle A recorded draw into a resident linear color target stored the whole image to guest memory right after the draw. Every draw copied the full surface, waited for the previous draw's store in the open batch (the pending-writer check of FlushPending), and the recorded copy landed over CPU stores made after Draw returned (#8152). A draw now records its scissor, clipped to the image, as a union rectangle in the target and lists the target as drawn. The listed targets are stored before each point where the guest can first observe the draws' completion: a flip (inside its locked block, before the flip's submit, so the flip's end-of-pipe interrupt and the presenter follow the store), a drain (which covers APS5_SYNC_FLIP=1), VulkanDevice::WaitIdle, a label the GPU writes for the graphics queue (WriteLabelOnGpu), and an interrupt-only RELEASE_MEM in preparePacketMemory and in commitLabel, which submit the store's batch before the interrupt can be delivered. The idle store, the access hooks and the eviction store are unchanged and also store by rectangle. A store of a drawn target copies the rectangle out of the image and writes it row by row at the guest pitch into the host import, stamped with one MarkWritten per run of 64 KiB blocks. A block that only other driver stores wrote since the target's generation keeps the old rule. In a block the CPU wrote, or that changed outside the write watch, only the rectangle's rows are stored, so CPU stores outside the rectangle stay, and the target re-uploads at its next use. A target with an attachment proxy, several mips or layers, a 3D or DCC surface, or results of a dispatch, fill or copy is stored whole at the same points. A store no longer waits for the same target's previous store: the pending-writer check skips the write notes of the target's own stores when it has a host import. APS5_PROFILE_DRAW adds the drawn targets' stores by reason and the pending drawn targets (count, MiB, oldest) to the [storage] line. The tests cover a target that stays pending across draws and labels until WaitIdle without a submission, #8152's CPU stores after Draw (ported from #8246, seeded and unseeded, inside and outside the write watch), CPU stores outside the drawn rectangle and in the gap before a target that starts past a 64 KiB boundary, and a flip, a drain, a label and an interrupt-only RELEASE_MEM through AgcDriver::Submit with the queue held by a WAIT_REG_MEM, inside and outside the write watch. Co-Authored-By: Claude --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 11 + .../src/Driver/Packets/PacketExecution.cpp | 2 + .../src/Driver/Packets/PacketMemory.cpp | 7 +- .../Execution/src/VulkanDevice.cpp | 2 + .../Graphics/include/Recorder.hpp | 1 + .../Graphics/include/Texture.hpp | 24 ++ .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 13 +- .../libSceAgcDriver/Graphics/src/Recorder.cpp | 20 + .../libSceAgcDriver/Graphics/src/Texture.cpp | 318 ++++++++++++++- .../LinearTargetCompletionPoints.cpp | 383 ++++++++++++++++++ .../LinearTargetCompletionStores.cpp | 304 ++++++++++++++ .../tests/execution/LinearTargetCpuWrites.cpp | 299 ++++++++++++++ .../execution/LinearTargetRectangleBound.cpp | 305 ++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 3 +- 14 files changed, 1666 insertions(+), 26 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCompletionPoints.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCompletionStores.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCpuWrites.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetRectangleBound.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index b2fcdefa8b..ff1b5232a6 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2080,6 +2080,7 @@ target_link_libraries(agc_driver_gpu_map_on_fiber_tests PRIVATE libSceFiber) target_link_libraries(agc_driver_rect_list_corner_tests PRIVATE shader_recompiler_agc_driver) target_link_libraries(agc_driver_queue_zero_late_interrupt_tests PRIVATE libkernel) +target_link_libraries(agc_driver_linear_target_completion_points_tests PRIVATE libkernel) target_link_libraries(agc_driver_storage_file_write_tests PRIVATE libkernel) @@ -2094,6 +2095,16 @@ target_include_directories(agc_driver_image_table_cache_bounds_tests PRIVATE $) if(BUILD_TESTING) + add_test(NAME agc_driver_linear_target_completion_points_sync_flip COMMAND agc_driver_linear_target_completion_points_tests --flip) + set_tests_properties(agc_driver_linear_target_completion_points_sync_flip PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SYNC_FLIP=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_driver_linear_target_completion_points_unpipelined COMMAND agc_driver_linear_target_completion_points_tests --label --interrupt) + set_tests_properties(agc_driver_linear_target_completion_points_unpipelined PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=0" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_linear_target_completion_points_sync_flip agc_driver_linear_target_completion_points_unpipelined PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_store_pending_identity COMMAND agc_driver_store_over_untracked_tests --pending-identity) set_tests_properties(agc_driver_store_pending_identity PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index a4c16c2410..e1627e26f4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -11,6 +11,7 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Eq/include/Event.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include #include #include @@ -398,6 +399,7 @@ void Driver::execute(const Submission& submission) { GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); std::lock_guard gpuLock(GuestMemory::GpuMutex()); const auto localDevice = device.Load(); + if (localDevice != nullptr) static_cast(Graphics::StorageTexture::StoreDrawnLinear("flip")); if (!recordLabelsForPacket(localDevice.get(), submission.queue) && localDevice != nullptr) localDevice->SubmitRecorded(submission.queue == 0); if (localDevice != nullptr) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp index 5c2cc06454..a20b86753f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -38,7 +38,7 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue std::lock_guard gpuLock(GuestMemory::GpuMutex()); const auto localDevice = device.Load(); recordDeferredLabels(localDevice.get(), submission.queue); - const bool workOpen = Graphics::Recorder::RecordedWorkSinceSubmit() != 0; + bool workOpen = Graphics::Recorder::RecordedWorkSinceSubmit() != 0; int reason = localDevice != nullptr ? 0 : 1; if (label.has_value()) { const auto bytes = label->Bytes(); @@ -52,6 +52,7 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue countLabelOutcome(reason); ++immediateLabels; } else { + if (localDevice != nullptr && submission.queue == 0 && Graphics::StorageTexture::StoreDrawnLinear("interrupt") != 0) workOpen = true; if (localDevice != nullptr) Graphics::StorageTexture::StoreReadbackBeforeLabel("readback before interrupt"); ++noOpLabels; } @@ -249,6 +250,7 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue recordDeferredLabels(draining.get(), submission.queue); if (draining != nullptr) { + static_cast(Graphics::StorageTexture::StoreDrawnLinear("drain")); if (unlockedDrain && draining->CanWaitUnlocked()) epoch = draining->SubmitAndEpoch(); else draining->WaitIdle(); } @@ -438,7 +440,7 @@ void Driver::commitLabel(std::uint32_t queue, std::uint64_t address, std::spanAfterRecordedWork([this, queue] { deliverGuestEopInterrupt(queue); }, queue == 0); if (deferred && workOpen) localDevice->SubmitRecorded(queue == 0); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 3030734ec7..2ac6b80b04 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2259,6 +2259,7 @@ void VulkanDevice::PrepareForReplacement() { void VulkanDevice::WaitIdle() { APS5_LOG_CHARS_OUT_DEBUG("VulkanDevice::WaitIdle begin"); + static_cast(Graphics::StorageTexture::StoreDrawnLinear("wait idle")); RetirePresents(0); if (state->recorder) { // Announced with this call's return address: the [recorder] site table then names the @@ -2354,6 +2355,7 @@ bool VulkanDevice::StoresPendingOver(std::uint64_t address, std::size_t bytes) c int VulkanDevice::WriteLabelOnGpu(std::uint64_t address, std::span bytes, std::uint64_t stamp, std::uint32_t queue, bool reapFirst) { static const bool storeAtLabel = Graphics::StorageTexture::StoreAtLabelRequested(std::getenv("APS5_STORE_AT_LABEL")); if (storeAtLabel) Graphics::StorageTexture::FlushAllPending("label"); + if (queue == 0) static_cast(Graphics::StorageTexture::StoreDrawnLinear("label")); Graphics::StorageTexture::StoreReadbackBeforeLabel("readback before label"); if (!state->recorder || bytes.empty() || bytes.size() > 65536 || address % 4 != 0 || bytes.size() % 4 != 0) return 4; auto& recorder = *state->recorder; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 6ef4b5cf08..f01282ed57 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -181,6 +181,7 @@ public: // Whether the OPEN (unsubmitted) batch writes the range: a wait on such a range must submit it. bool OpenWriteOverlaps(std::uint64_t address, std::size_t bytes) const; std::uint64_t PendingWriterSerial(std::uint64_t address, std::size_t bytes) const; + std::uint64_t PendingWriterSerial(std::uint64_t address, std::size_t bytes, std::span exceptNotes) const; // What SyncThrough(address, bytes) would wait for, for the [hooksync] attribution: the newest // batch whose noted range overlaps the access (the open batch counts first, with the serial it // will get), whether that batch's fence has already signaled (its GPU work is done, so the diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 4dde83053f..aa4cc1d36c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -203,6 +203,7 @@ public: bool BlockUnits() const { return blockUnits; } std::uint32_t TrackedUnits() const { return trackedLayers; } void MarkSliceDirty(std::uint32_t slice, std::uint32_t count = 1); + void MarkDrawn(std::uint32_t slice, std::uint32_t count, VkRect2D scissor); void Flush(); bool Dirty() const; // Stores every pending image overlapping the range, except `except`; returns whether any was. @@ -219,6 +220,7 @@ public: static constexpr std::uint64_t SmallStoreBytes = 64 * 1024; static std::size_t StoreSmallPending(const char* reason); static void StoreReadbackBeforeLabel(const char* reason); + static std::size_t StoreDrawnLinear(const char* reason); static bool AnyPending(); static void FlushDumpSelected(const char* reason); static bool StoreAtFlipRequested(const char* value); @@ -480,6 +482,19 @@ private: // (`shadowed` receives those guest ranges, `imported` the pieces written to the import). std::uint64_t uploadWindows(const HostImport& import, std::span> runs, bool discard = false); std::uint64_t writeBackWindows(std::span segments, std::span> keep, std::uint64_t firstStored, std::uint64_t lastStored, std::vector& shadowed, std::vector>& imported, Buffer* save = nullptr); + struct DrawnArea { + bool drawn = false; + bool rect = false; + std::uint32_t left = 0; + std::uint32_t top = 0; + std::uint32_t right = 0; + std::uint32_t bottom = 0; + }; + DrawnArea drawnArea() const; + std::vector> drawnSpans(const DrawnArea& area, std::span> keep) const; + std::uint64_t storeDrawnRect(const DrawnArea& area, std::span segments, std::span> stores, const std::vector& saveUnits); + void forgetDrawn(); + void noteStore(std::uint64_t note); // Per-layer validity (thin array surfaces whose layers are 64 KiB-aligned guest slices; every // other surface is one tracked layer): a layer's generation is the write generation its guest // bytes were last known to match the image at, and a pending layer holds results (a clear, a @@ -639,6 +654,15 @@ private: VkFormat storageFormat = VK_FORMAT_UNDEFINED; // Results are on the GPU only (guarded by the pending-write registry lock). bool dirty = false; + bool drawnListed = false; + bool drawnWhole = false; + bool drawingTarget = false; + bool pendingFromOthers = false; + std::uint32_t drawnLeft = 0; + std::uint32_t drawnTop = 0; + std::uint32_t drawnRight = 0; + std::uint32_t drawnBottom = 0; + std::vector storeNotes; std::uint64_t version = 0; std::uint64_t writes = 0; // `original` holds the guest bytes; false after a GPU-side clear, which never read them. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 4a0aadae0d..005e697446 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1682,7 +1682,7 @@ struct Kept { // The completion side of a recorded draw: the kept objects, the record check, the lease outcome, // the GPU write notes, the copied-buffer write-back (listed in DrawCopiedWriters or run as a // completion action) and the targets marked dirty. -void keepRecordedDraw(Recorder& recorder, const std::shared_ptr& resources, std::shared_ptr pipeline, std::shared_ptr framebuffer, DrawInputs& inputs, std::vector> targets, std::vector> slices, std::unique_ptr scratch, std::function checkRecords, bool listed, bool completion, const DrawOutcome& outcome) { +void keepRecordedDraw(Recorder& recorder, const std::shared_ptr& resources, std::shared_ptr pipeline, std::shared_ptr framebuffer, DrawInputs& inputs, std::vector> targets, std::vector> slices, VkRect2D scissor, std::unique_ptr scratch, std::function checkRecords, bool listed, bool completion, const DrawOutcome& outcome) { auto kept = std::make_shared(); kept->resources = resources; kept->pipeline = std::move(pipeline); @@ -1724,13 +1724,10 @@ void keepRecordedDraw(Recorder& recorder, const std::shared_ptr } for (std::size_t index = 0; index < residents.size(); ++index) { if (residents[index] == nullptr) continue; - if (index < slices.size()) residents[index]->MarkSliceDirty(slices[index].first, slices[index].second); + if (residents[index]->Descriptor().tileMode == TextureTileMode::kLinear) residents[index]->MarkDrawn(index < slices.size() ? slices[index].first : 0u, index < slices.size() ? slices[index].second : 0u, scissor); + else if (index < slices.size()) residents[index]->MarkSliceDirty(slices[index].first, slices[index].second); else residents[index]->MarkDirty(); } - for (const auto& resident : residents) { - if (resident == nullptr || resident->Descriptor().tileMode != TextureTileMode::kLinear) continue; - StorageTexture::FlushPending(resident->Descriptor().baseAddress, static_cast(resident->GuestBytes()), nullptr, "linear target"); - } } // What recordDraw records from: the recorder, the resources, the pipeline and framebuffer, the @@ -2117,7 +2114,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam } recorder->LeaveRenderPassOpen(passKey, drawTiming, fenced, passAccess, std::move(storeProxies)); timer.phase(PhaseRecord); - keepRecordedDraw(*recorder, record.resources, std::move(record.pipeline), std::move(record.framebuffer), inputs, std::move(record.targets), std::move(record.targetSlices), std::move(scratch), std::move(checkRecords), record.listed, record.completion, outcome); + keepRecordedDraw(*recorder, record.resources, std::move(record.pipeline), std::move(record.framebuffer), inputs, std::move(record.targets), std::move(record.targetSlices), state.scissor, std::move(scratch), std::move(checkRecords), record.listed, record.completion, outcome); timer.phase(PhaseKeep); if (record.waited) { // The wait the dispatch path makes for such work (source 3, "address-based"): the @@ -2872,7 +2869,7 @@ void recordPreparedDraw(const Context& context, const State& state, const Pm4::D APS5_LOG_CHARS_OUT_DEBUG("SubmitAndWait"); timer.phase(PhaseRecord); if (recorded) { - keepRecordedDraw(*recorder, resources, pipeline, framebuffer, inputs, owners, [&] { std::vector> slices; for (const auto& binding : targets) slices.emplace_back(binding.color.arraySlice, binding.color.arrayLastSlice != 0 ? binding.color.layers : 1u); return slices; }(), std::move(scratch), std::move(checkRecords), listed, completion, outcome); + keepRecordedDraw(*recorder, resources, pipeline, framebuffer, inputs, owners, [&] { std::vector> slices; for (const auto& binding : targets) slices.emplace_back(binding.color.arraySlice, binding.color.arrayLastSlice != 0 ? binding.color.layers : 1u); return slices; }(), state.scissor, std::move(scratch), std::move(checkRecords), listed, completion, outcome); timer.phase(PhaseKeep); if (outcome.waited) { // The wait the dispatch path makes for such work (source 3, "address-based"): the diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index cfbb3c7b06..af1a7cd689 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -3069,6 +3069,26 @@ std::uint64_t Recorder::PendingWriterSerial(std::uint64_t address, std::size_t b return 0; } +std::uint64_t Recorder::PendingWriterSerial(std::uint64_t address, std::size_t bytes, std::span exceptNotes) const { + if (exceptNotes.empty()) return PendingWriterSerial(address, bytes); + if (bytes == 0) return 0; + const auto end = address + bytes; + const auto writes = [&](const Batch& batch) { + for (std::size_t index = 0; index < batch.writes.size(); ++index) { + const auto& [begin, finish] = batch.writes[index]; + if (address >= finish || begin >= end) continue; + if (index < batch.writeNotes.size() && std::find(exceptNotes.begin(), exceptNotes.end(), batch.writeNotes[index]) != exceptNotes.end()) continue; + return true; + } + return readOverlap(batch, address, end, ReadKind::AddressBased).has_value(); + }; + if (open != nullptr && writes(*open)) return OpenSerial(); + for (auto it = inFlight.rbegin(); it != inFlight.rend(); ++it) { + if (writes(**it)) return (*it)->serial; + } + return 0; +} + bool Recorder::signaled(const Batch& batch) const { return batch.submitted && fenceStatus(batch.fence) == VK_SUCCESS; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index abfda95644..f9a5171189 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -122,6 +122,7 @@ struct StorageTraffic { std::map> writeBacks; std::array, 4> uploads{}; std::map> uploadReasons; + std::map> drawnStores; std::uint64_t directWriteBacks = 0; std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now(); }; @@ -131,6 +132,14 @@ StorageTraffic& Traffic() { return traffic; } +struct DrawnPending { + std::size_t images = 0; + std::uint64_t bytes = 0; + double oldestMs = 0; +}; + +DrawnPending drawnPending(); + void reportStorageTraffic(StorageTraffic& traffic) { const auto now = std::chrono::steady_clock::now(); if (now - traffic.lastReport < std::chrono::seconds(10)) return; @@ -153,12 +162,19 @@ void reportStorageTraffic(StorageTraffic& traffic) { std::snprintf(text, sizeof(text), " %llu/%.1f", static_cast(totals.first), totals.second / 1048576.0); line += " " + reason + text; } + std::string drawnLine; + for (const auto& [reason, totals] : traffic.drawnStores) { + std::snprintf(text, sizeof(text), " %llu/%.1f", static_cast(totals.first), totals.second / 1048576.0); + drawnLine += " " + reason + text; + } + const auto drawn = drawnPending(); const auto take = [](std::atomic& counter) { return static_cast(counter.exchange(0, std::memory_order_relaxed)); }; const auto partialUploadCount = take(partialUploads), partialWriteBackCount = take(partialWriteBacks); const auto uploadMiB = take(partialUploadBytes) / 1048576.0, writeBackMiB = take(partialWriteBackBytes) / 1048576.0; - AgcDriver::ProfilePrint_nid_no_patch( "[storage] uploads by path (count/MiB, 10 s):%s; %llu write-backs GPU-direct, %llu stored nothing; block units: partial uploads %llu/%.1f, partial write-backs %llu/%.1f, %llu widened to every pending unit, units dropped %llu, superseded %llu, handed over at %llu clear-key refreshes (%llu units superseded), dropped unregistered %llu, kept over a key flip %llu%s, pretest skipped %llu, held for a queued draw read %llu, evicted stale %llu\n", line.c_str(), static_cast(traffic.directWriteBacks), take(emptyWriteBacks), partialUploadCount, uploadMiB, partialWriteBackCount, writeBackMiB, take(coalescedWriteBacks), take(unitsDropped), take(unitsSuperseded), take(clearHandOvers), take(handedOverSuperseded), take(unregisteredDropped), take(keyFlipKept), ShadowReport().c_str(), take(pretestSkipped), take(queuedReadHeld), take(staleEvicted)); + AgcDriver::ProfilePrint_nid_no_patch( "[storage] uploads by path (count/MiB, 10 s):%s; %llu write-backs GPU-direct, %llu stored nothing; block units: partial uploads %llu/%.1f, partial write-backs %llu/%.1f, %llu widened to every pending unit, units dropped %llu, superseded %llu, handed over at %llu clear-key refreshes (%llu units superseded), dropped unregistered %llu, kept over a key flip %llu%s, pretest skipped %llu, held for a queued draw read %llu, evicted stale %llu; drawn linear targets: stores by reason (count/MiB):%s, pending %zu images / %.1f MiB, oldest %.1f ms\n", line.c_str(), static_cast(traffic.directWriteBacks), take(emptyWriteBacks), partialUploadCount, uploadMiB, partialWriteBackCount, writeBackMiB, take(coalescedWriteBacks), take(unitsDropped), take(unitsSuperseded), take(clearHandOvers), take(handedOverSuperseded), take(unregisteredDropped), take(keyFlipKept), ShadowReport().c_str(), take(pretestSkipped), take(queuedReadHeld), take(staleEvicted), drawnLine.c_str(), drawn.images, drawn.bytes / 1048576.0, drawn.oldestMs); traffic.writeBacks.clear(); traffic.uploadReasons.clear(); + traffic.drawnStores.clear(); traffic.directWriteBacks = 0; } @@ -174,7 +190,7 @@ void countStorageUpload(std::size_t path, std::uint64_t bytes) { reportStorageTraffic(traffic); } -void countStorageWriteBack(std::uint64_t bytes, bool direct) { +void countStorageWriteBack(std::uint64_t bytes, bool direct, bool drawn = false) { if (!LookupOutcomes::Profiled()) return; auto& traffic = Traffic(); std::lock_guard lock(traffic.mutex); @@ -182,6 +198,11 @@ void countStorageWriteBack(std::uint64_t bytes, bool direct) { ++totals.first; totals.second += bytes; if (direct) ++traffic.directWriteBacks; + if (drawn) { + auto& stores = traffic.drawnStores[flushReason != nullptr ? flushReason : "other"]; + ++stores.first; + stores.second += bytes; + } reportStorageTraffic(traffic); } @@ -1532,6 +1553,13 @@ struct PendingWrites { // Images taken out of `textures` by a FlushPending still storing them (see adjacentPendingUnchanged). std::vector flushing; std::atomic flushingCount{0}; + struct Drawn { + StorageTexture* texture; + std::uint64_t bytes; + std::chrono::steady_clock::time_point since; + }; + std::vector drawn; + std::atomic drawnCount{0}; }; PendingWrites& Pending() { @@ -1539,6 +1567,20 @@ PendingWrites& Pending() { return pending; } +DrawnPending drawnPending() { + DrawnPending result; + auto& pending = Pending(); + if (pending.drawnCount.load(std::memory_order_acquire) == 0) return result; + const auto now = std::chrono::steady_clock::now(); + std::lock_guard lock(pending.mutex); + result.images = pending.drawn.size(); + for (const auto& entry : pending.drawn) { + result.bytes += entry.bytes; + result.oldestMs = std::max(result.oldestMs, std::chrono::duration(now - entry.since).count()); + } + return result; +} + // See StorageTexture::PendingSerial: bumped after every mutation of the registry above. std::atomic pendingSerial{0}; @@ -3815,6 +3857,10 @@ void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) } auto& pending = Pending(); std::lock_guard lock(pending.mutex); + if (!drawingTarget) { + pendingFromOthers = true; + if (drawnListed) drawnWhole = true; + } // The active recorder installs a hook that also waits for recorded work; without one (tests), // pending storage results alone are flushed. if (Recorder::Active() == nullptr) { @@ -3839,6 +3885,7 @@ void StorageTexture::reconcilePending() { auto& pending = Pending(); std::lock_guard lock(pending.mutex); const bool any = anyLayerPending(); + if (!any) forgetDrawn(); if (any == dirty) return; dirty = any; if (any) pending.textures.push_back(this); @@ -3852,6 +3899,184 @@ bool StorageTexture::Dirty() const { return dirty; } +void StorageTexture::MarkDrawn(std::uint32_t slice, std::uint32_t count, VkRect2D scissor) { + struct Drawing { + bool& flag; + bool previous; + ~Drawing() { flag = previous; } + } drawing{drawingTarget, std::exchange(drawingTarget, true)}; + MarkSliceDirty(slice, count); + const auto clip = [](std::int64_t value, std::uint32_t limit) { return static_cast(std::clamp(value, 0, limit)); }; + const auto left = clip(scissor.offset.x, descriptor.width); + const auto top = clip(scissor.offset.y, descriptor.height); + const auto right = clip(std::int64_t{scissor.offset.x} + scissor.extent.width, descriptor.width); + const auto bottom = clip(std::int64_t{scissor.offset.y} + scissor.extent.height, descriptor.height); + const bool rectangle = proxyImage == VK_NULL_HANDLE && descriptor.tileMode == TextureTileMode::kLinear && !blockUnits && trackedLayers == 1 && descriptor.mipCount == 1 && arrayLayers == 1 && geometry.imageLayers == 1 && geometry.imageDepth == 1 && !geometry.thick && descriptor.dccAddress == 0; + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + if (!dirty) return; + if (!drawnListed) { + drawnListed = true; + drawnWhole = false; + drawnLeft = drawnTop = drawnRight = drawnBottom = 0; + pending.drawn.push_back({this, guestBytes, std::chrono::steady_clock::now()}); + pending.drawnCount.store(pending.drawn.size(), std::memory_order_release); + } + if (!rectangle || pendingFromOthers) drawnWhole = true; + if (drawnWhole || right <= left || bottom <= top) return; + if (drawnRight <= drawnLeft || drawnBottom <= drawnTop) { + drawnLeft = left; + drawnTop = top; + drawnRight = right; + drawnBottom = bottom; + return; + } + drawnLeft = std::min(drawnLeft, left); + drawnTop = std::min(drawnTop, top); + drawnRight = std::max(drawnRight, right); + drawnBottom = std::max(drawnBottom, bottom); +} + +void StorageTexture::forgetDrawn() { + pendingFromOthers = false; + if (!drawnListed) return; + drawnListed = false; + drawnWhole = false; + drawnLeft = drawnTop = drawnRight = drawnBottom = 0; + auto& pending = Pending(); + std::erase_if(pending.drawn, [this](const PendingWrites::Drawn& entry) { return entry.texture == this; }); + pending.drawnCount.store(pending.drawn.size(), std::memory_order_release); +} + +void StorageTexture::noteStore(std::uint64_t note) { + if (note == 0) return; + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + storeNotes.push_back(note); + if (storeNotes.size() > 8) storeNotes.erase(storeNotes.begin()); +} + +StorageTexture::DrawnArea StorageTexture::drawnArea() const { + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + if (!drawnListed) return {}; + return {true, !drawnWhole && !pendingFromOthers, drawnLeft, drawnTop, drawnRight, drawnBottom}; +} + +std::vector> StorageTexture::drawnSpans(const DrawnArea& area, std::span> keep) const { + std::vector> spans; + if (area.right <= area.left || area.bottom <= area.top) return spans; + const auto& mip = mips.front(); + const auto elementBytes = static_cast(BytesPerElement(descriptor.format)); + const auto rowBase = descriptor.baseAddress + mip.tiledOffset; + std::size_t next = 0; + for (auto row = area.top; row < area.bottom; ++row) { + const auto rowStart = rowBase + std::uint64_t{row} * mip.pitchBytes; + const auto rowBegin = rowStart + area.left * elementBytes; + const auto rowEnd = rowStart + area.right * elementBytes; + while (next < keep.size() && keep[next].second <= rowBegin) ++next; + for (auto index = next; index < keep.size() && keep[index].first < rowEnd; ++index) { + const auto from = std::max(rowBegin, keep[index].first); + const auto to = std::min(rowEnd, keep[index].second); + if (from < to) spans.emplace_back(from, to); + } + } + return spans; +} + +std::uint64_t StorageTexture::storeDrawnRect(const DrawnArea& area, std::span segments, std::span> stores, const std::vector& saveUnits) { + const auto& mip = mips.front(); + const auto elementBytes = static_cast(BytesPerElement(descriptor.format)); + const auto rowBase = descriptor.baseAddress + mip.tiledOffset; + const auto width = area.right - area.left; + const auto height = area.bottom - area.top; + const auto rowBytes = std::uint64_t{width} * elementBytes; + const auto hullBegin = stores.front().first; + const auto hullBytes = static_cast(stores.back().second - hullBegin); + std::uint64_t storedBytes = 0; + for (const auto& [from, to] : stores) storedBytes += to - from; + auto scratch = std::make_shared(context, static_cast(rowBytes * height), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); + auto* recorder = Recorder::Active(); + std::unique_ptr batch; + VkCommandBuffer commands = VK_NULL_HANDLE; + auto timing = Recorder::NoTiming; + if (recorder != nullptr) { + recorder->FlushKeyStoresOverlapping(hullBegin, hullBytes); + recorder->FlushStoresOverlapping(hullBegin, hullBytes); + commands = recorder->Commands(); + timing = recorder->BeginGpuTiming(Recorder::CommandClass::StorageWriteBack); + recorder->Keep(scratch, scratch->Size()); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); + Recorder::CountBarriers(Recorder::CommandClass::StorageWriteBack, 3); + if (Recorder::BarrierValidate()) { + const std::pair read{image, false}; + recorder->NoteAccess(Recorder::CommandClass::StorageWriteBack, Recorder::Access{{}, stores, std::span(&read, 1), VK_PIPELINE_STAGE_TRANSFER_BIT}); + } + } else { + batch = std::make_unique(context); + commands = batch->Handle(); + } + VkImageMemoryBarrier toSource{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toSource.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + toSource.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + toSource.oldLayout = VK_IMAGE_LAYOUT_GENERAL; + toSource.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + toSource.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toSource.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toSource.image = image; + toSource.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, descriptor.mipCount, 0, geometry.imageLayers}; + const VkMemoryBarrier importReady{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT}; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &importReady, 0, nullptr, 1, &toSource); + VkBufferImageCopy region{}; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.imageOffset = {static_cast(area.left), static_cast(area.top), 0}; + region.imageExtent = {width, height, 1u}; + context.Function("vkCmdCopyImageToBuffer")(commands, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, scratch->Handle(), 1, ®ion); + const auto scratchRead = WholeBufferBarrier(scratch->Handle(), VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &scratchRead, 0, nullptr); + const auto scratchOffset = [&](std::uint64_t address) { + const auto offset = address - rowBase; + const auto row = offset / mip.pitchBytes; + return (row - area.top) * rowBytes + offset - row * mip.pitchBytes - area.left * elementBytes; + }; + const auto copyBuffer = context.Function("vkCmdCopyBuffer"); + for (const auto& segment : segments) { + std::vector copies; + for (const auto& [from, to] : stores) { + const auto begin = std::max(from, segment.begin); + const auto end = std::min(to, segment.end); + if (begin < end) copies.push_back({scratchOffset(begin), HostImportOffset(*segment.import, begin), end - begin}); + } + if (!copies.empty()) copyBuffer(commands, scratch->Handle(), segment.import->buffer, static_cast(copies.size()), copies.data()); + } + if (!saveUnits.empty()) { + const auto saved = savedBytes->Bytes(); + for (const auto unit : saveUnits) { + const auto offset = static_cast(layerBegin(unit) - descriptor.baseAddress); + std::memcpy(saved.data() + offset, original.data() + offset, static_cast(layerBytes(unit))); + } + std::vector saves; + for (const auto& [from, to] : stores) saves.push_back({scratchOffset(from), from - descriptor.baseAddress, to - from}); + copyBuffer(commands, scratch->Handle(), savedBytes->Handle(), static_cast(saves.size()), saves.data()); + } + VkImageMemoryBarrier backToGeneral = toSource; + backToGeneral.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + backToGeneral.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; + backToGeneral.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + backToGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; + const VkMemoryBarrier stored{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_READ_BIT}; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &stored, 0, nullptr, 1, &backToGeneral); + if (batch) { + batch->SubmitAndWait(); + return storedBytes; + } + recorder->EndGpuTiming(timing, storedBytes); + recorder->MarkShaderReadsCovered(); + recorder->NotePendingWrite(hullBegin, hullBytes); + noteStore(recorder->LastWriteNote(hullBegin, hullBytes)); + return storedBytes; +} + void StorageTexture::Flush() { { auto& live = Live(); @@ -3909,6 +4134,7 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons // The images stay alive across the scan: their last owner may be a batch's kept list, which // another thread releases outside the GPU mutex once the batch completed. std::vector> flush; + std::vector ownNotes; { auto& pending = Pending(); std::lock_guard lock(pending.mutex); @@ -3936,6 +4162,9 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons for (const auto& texture : flush) pending.flushing.push_back(texture.get()); pending.flushingCount.store(pending.flushing.size(), std::memory_order_release); if (!flush.empty()) BumpPendingSerial(); + for (const auto& texture : flush) { + if (texture->drawnListed) ownNotes.insert(ownNotes.end(), texture->storeNotes.begin(), texture->storeNotes.end()); + } } if (flush.empty()) { const bool stored = WaitForFlushing(address, bytes, except); @@ -3961,10 +4190,11 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons for (const auto& texture : flush) std::fprintf(stderr, "[flush] image 0x%llx+0x%llx for %s 0x%llx+0x%zx\n", static_cast(texture->descriptor.baseAddress), static_cast(texture->guestBytes), reason, static_cast(address), bytes); } std::lock_guard gpu(GuestMemory::GpuMutex()); - if (auto* recorder = Recorder::Active(); recorder != nullptr && !Recorder::InCompletion() && recorder->PendingWriterSerial(address, bytes) != 0) { + if (!ownNotes.empty() && std::any_of(flush.begin(), flush.end(), [](const std::shared_ptr& texture) { return ExistingHostImportFor(texture->context, texture->descriptor.baseAddress, static_cast(texture->guestBytes)) == nullptr; })) ownNotes.clear(); + if (auto* recorder = Recorder::Active(); recorder != nullptr && !Recorder::InCompletion() && recorder->PendingWriterSerial(address, bytes, ownNotes) != 0) { try { recorder->Reap(); - if (const auto writer = recorder->PendingWriterSerial(address, bytes); writer == recorder->OpenSerial()) { + if (const auto writer = recorder->PendingWriterSerial(address, bytes, ownNotes); writer == recorder->OpenSerial()) { Recorder::CountSync(recorder->OpenWriteOverlaps(address, bytes) ? 1 : 3); recorder->Sync(); } else if (writer != 0) { @@ -4246,6 +4476,25 @@ void StorageTexture::StoreReadbackBeforeLabel(const char* reason) { for (const auto& [address, bytes] : ranges) static_cast(FlushPending(address, bytes, nullptr, reason)); } +std::size_t StorageTexture::StoreDrawnLinear(const char* reason) { + auto& pending = Pending(); + if (pending.drawnCount.load(std::memory_order_acquire) == 0 || Recorder::InCompletion()) return 0; + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); + std::lock_guard gpu(GuestMemory::GpuMutex()); + std::vector> ranges; + { + std::lock_guard lock(pending.mutex); + for (const auto& entry : pending.drawn) { + if (entry.texture->dirty) ranges.emplace_back(entry.texture->descriptor.baseAddress, static_cast(entry.texture->guestBytes)); + } + } + std::size_t stored = 0; + for (const auto& [address, bytes] : ranges) { + if (FlushPending(address, bytes, nullptr, reason)) ++stored; + } + return stored; +} + bool StorageTexture::AnyPending() { auto& pending = Pending(); std::lock_guard lock(pending.mutex); @@ -4553,6 +4802,7 @@ std::size_t StorageTexture::DiscardPendingInside(std::uint64_t address, std::siz texture->layerPending.assign(texture->trackedLayers, false); texture->layerOriginal.assign(texture->trackedLayers, false); texture->cancelStoredSaves(); + texture->forgetDrawn(); it = pending.textures.erase(it); ++discarded; } else if (texture != exempt && texture->blockUnits && texture->overlaps(address, bytes)) { @@ -5460,8 +5710,10 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { const auto end = layerBegin(layer) + layerBytes(layer); for (auto at = layerBegin(layer) & ~(block - 1); at < end; at += block) generations[static_cast((at - spanBegin) / block)] = layerGeneration[layer]; } + const auto drawn = drawnArea(); std::vector changedBlocks(spanBlocks); - const bool tracked = GuestMemory::ChangedBlocks(firstStored, static_cast(lastStored - firstStored), generations, changedBlocks); + std::vector cpuBlocks(drawn.drawn ? spanBlocks : 0); + const bool tracked = GuestMemory::ChangedBlocks(firstStored, static_cast(lastStored - firstStored), generations, changedBlocks, cpuBlocks); std::vector compared(trackedLayers, false); if (!tracked) { awaitStoredSave(&layers); @@ -5486,7 +5738,8 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { if (!layers[layer]) continue; const auto begin = layerBegin(layer); const auto end = begin + layerBytes(layer); - if (tracked ? layerGeneration[layer] == 0 : !compared[layer]) { + const bool unknown = tracked ? layerGeneration[layer] == 0 : !compared[layer]; + if (unknown && !drawn.rect) { unsavedOutsideWatch = unsavedOutsideWatch || !tracked; keep.emplace_back(begin, end); continue; @@ -5496,12 +5749,16 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { const auto to = std::min(at + block, end); if (from >= to) continue; const bool edge = from != at || to != at + block; - if (keptAtUnwatchHolds(from, layer) || (changedBlocks[static_cast((at - spanBegin) / block)] == GuestMemory::BlockWritten && (compared[layer] || !edge || GuestMemory::StoredOver(from, static_cast(to - from), layerGeneration[layer])) && !unchangedSinceBaseline(from, to))) { + const auto index = static_cast((at - spanBegin) / block); + const bool kept = keptAtUnwatchHolds(from, layer); + const bool written = kept || unknown || (changedBlocks[index] == GuestMemory::BlockWritten && (compared[layer] || !edge || GuestMemory::StoredOver(from, static_cast(to - from), layerGeneration[layer])) && !unchangedSinceBaseline(from, to)); + if (written && (!drawn.drawn || (tracked && !kept && !unknown && cpuBlocks[index] == 0))) { skippedAny = true; skippedLayer[layer] = true; ++skipped; continue; } + if (written && drawn.rect) skippedLayer[layer] = true; if (!keep.empty() && keep.back().second == from) keep.back().second = to; else keep.emplace_back(from, to); } @@ -5515,10 +5772,15 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { const auto first = keep.empty() ? descriptor.baseAddress + guestBytes : keep.front().first; std::fprintf(stderr, "[flush] image 0x%llx+0x%llx keeps %zu CPU-written 64 KiB blocks (first stored byte at +0x%llx, %zu ranges stored, generation %llu)\n", static_cast(descriptor.baseAddress), static_cast(guestBytes), skipped, static_cast(first - descriptor.baseAddress), keep.size(), static_cast(generation)); } + std::vector> spans; + if (drawn.rect) spans = drawnSpans(drawn, keep); + const auto& stores = drawn.rect ? spans : keep; + const auto stampBegin = drawn.rect && !stores.empty() ? stores.front().first : firstStored; + const auto stampEnd = drawn.rect && !stores.empty() ? stores.back().second : lastStored; // Taken before this write-back stamps anything (see advanceAdjacent), with the 64 KiB block span // its stamps cover. - const auto firstBlock = firstStored / block; - const auto lastBlock = (lastStored - 1) / block; + const auto firstBlock = stampBegin / block; + const auto lastBlock = (stampEnd - 1) / block; const auto adjacent = adjacentPendingUnchanged(firstBlock, lastBlock); // After the store: the stored layers are in sync at a fresh generation unless blocks were kept // for the CPU (then the layer is stale there, keeps its old generation and re-uploads at its @@ -5555,7 +5817,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { advanceAdjacent(adjacent, now, firstBlock, lastBlock); if (!shadowed.empty()) markShadowed(now); }; - if (keep.empty()) { + if (stores.empty()) { // Every selected block was written by the CPU since the layer's generation: nothing to // store, the keys stay as they are, and the image is stale there (re-uploaded at its next // use, never from a matching `original`). @@ -5585,6 +5847,24 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { ++Profile().storageDirectWriteBacks; return; } + if (drawn.rect) { + PublishShadow(stampBegin, static_cast(stampEnd - stampBegin), PublishScope::PartialUnits, PublishReason::Upload); + const auto storedBytes = storeDrawnRect(drawn, segments, stores, saveUnits); + countStorageWriteBack(storedBytes, true, true); + if (profile) Profile().storageGpu += timer.lap(); + forgetStored(); + uploadedKeys = DccKeys::Uncompressed; + std::vector> stamps; + for (const auto& [from, to] : stores) { + if (!stamps.empty() && from / block <= (stamps.back().second - 1) / block + 1) stamps.back().second = to; + else stamps.emplace_back(from, to); + } + for (const auto& [from, to] : stamps) GuestMemory::MarkWritten(from, static_cast(to - from)); + settle(true); + commitStoredSave(saveUnits); + ++Profile().storageDirectWriteBacks; + return; + } // The whole-layer copies below overwrite the interior units of the span; the edge units // keep a neighbour's shadowed results in the rest of their bytes, which reach the import // first (this write-back's stamps then make those units stale). @@ -5687,8 +5967,9 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { recorder->EndGpuTiming(timing, storedBytes); recorder->MarkShaderReadsCovered(); recorder->NotePendingWrite(firstStored, static_cast(lastStored - firstStored)); + if (drawn.drawn) noteStore(recorder->LastWriteNote(firstStored, static_cast(lastStored - firstStored))); } - countStorageWriteBack(storedBytes, true); + countStorageWriteBack(storedBytes, true, drawn.drawn); if (profile) Profile().storageGpu += timer.lap(); // The guest bytes now differ from `original`; other caches of the range see the write. forgetStored(); @@ -5729,10 +6010,10 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { overhang = covered >= end; } } - const bool registered = std::all_of(keep.begin(), keep.end(), [](const auto& range) { return RegisteredReadableCovers(range.first, static_cast(range.second - range.first)); }); + const bool registered = std::all_of(stores.begin(), stores.end(), [](const auto& range) { return RegisteredReadableCovers(range.first, static_cast(range.second - range.first)); }); if (unregisteredDrop && !overhang && !registered) { static std::atomic reports{0}; - if (reports.fetch_add(1) < 4) std::fprintf(stderr, "[gpu] storage image 0x%llx+0x%llx: memory no longer registered; %zu pending ranges dropped\n", static_cast(descriptor.baseAddress), static_cast(guestBytes), keep.size()); + if (reports.fetch_add(1) < 4) std::fprintf(stderr, "[gpu] storage image 0x%llx+0x%llx: memory no longer registered; %zu pending ranges dropped\n", static_cast(descriptor.baseAddress), static_cast(guestBytes), stores.size()); unregisteredDropped.fetch_add(1, std::memory_order_relaxed); forgetStored(); for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { @@ -5749,6 +6030,10 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { std::vector unsaved = layerOriginal; unsaved.flip(); for (const auto& [begin, end] : unitRuns(unsaved)) { + if (drawn.rect) { + GuestMemory::ReadCommitted(descriptor.baseAddress + begin, std::span(original).subspan(static_cast(begin), static_cast(end - begin))); + continue; + } for (const auto& [from, to] : keep) { const auto first = std::max(descriptor.baseAddress + begin, from); const auto last = std::min(descriptor.baseAddress + end, to); @@ -5810,7 +6095,9 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { APS5_LOG_CHARS_OUT("StorageTexture writeback submit"); batch.SubmitAndWait(); APS5_LOG_CHARS_OUT("StorageTexture writeback done"); - countStorageWriteBack(guestBytes, false); + std::uint64_t storedBytes = 0; + for (const auto& [from, to] : stores) storedBytes += to - from; + countStorageWriteBack(drawn.rect ? storedBytes : guestBytes, false, drawn.drawn); if (profile) Profile().storageGpu += timer.lap(); if (dump) { char name[64]; @@ -5825,7 +6112,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { // Store only the kept ranges' blocks that changed, so concurrent CPU writes to untouched texels // survive; `original` follows for the stored layers (the others' guest bytes are unchanged). const auto current = host.Bytes(); - for (const auto& [from, to] : keep) { + for (const auto& [from, to] : stores) { const auto offset = static_cast(from - descriptor.baseAddress); const auto length = static_cast(to - from); GuestMemory::WriteChangedCommitted(from, current.subspan(offset, length), std::span(original).subspan(offset, length)); @@ -5861,6 +6148,7 @@ StorageTexture::~StorageTexture() { { auto& pending = Pending(); std::lock_guard lock(pending.mutex); + forgetDrawn(); if (dirty) { dirty = false; pending.textures.remove(this); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCompletionPoints.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCompletionPoints.cpp new file mode 100644 index 0000000000..558b04df4b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCompletionPoints.cpp @@ -0,0 +1,383 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" +#include "prx/libSceAgcDriver/Eq/include/Event.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "prx/libkernel/Equeue/Equeue.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name); +extern "C" int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq); + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Width = 256; +constexpr std::uint32_t Height = 128; +constexpr std::size_t Pitch = std::size_t{Width} * 4u; +constexpr std::size_t TargetBytes = Pitch * Height; +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t LabelOffset = 3 * BlockBytes; +constexpr std::uint32_t Untouched = 0x5a5a5a5au; +constexpr std::uint32_t Drawn = 0xff0000ffu; +constexpr std::uint32_t LabelValue = 0x600dcafeu; +constexpr std::uint32_t OutputHandle = 9; +constexpr std::uint32_t Rgba8UnormInfo = 0x28028u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::array Scissor{32, 16, 160, 80}; + +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060208u, 0x7e0802f2u, 0x7e0a02ffu, 0x44800000u, 0xf80000d1u, 0x00000005u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; +alignas(256) constexpr std::array FillCode{ + 0xd7460004, 0x04010c08, 0x7e000204, 0x7e020205, 0x7e040206, 0x7e060207, 0xe01c2000, 0x80000004, 0xbf810000, +}; + +alignas(4) std::atomic Gate = 0; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; + +void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +void AppendDraw(std::vector& commands, std::uint64_t target) { + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const auto extent = (Height << 16u) | Width; + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0x01200000}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, Rgba8UnormInfo}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, ((Width - 1u) << 14u) | (Height - 1u)}, + {0x3b8, LinearAttrib3}, {0x1e0, 0}, + {0xc, 0}, {0xd, extent}, + {0x81, 0x80000000}, {0x82, extent}, + {0x90, 0x80000000u | (Scissor[1] << 16u) | Scissor[0]}, {0x91, (Scissor[3] << 16u) | Scissor[2]}, + {0x94, 0x80000000}, {0x95, extent}, + {0x10f, std::bit_cast(2.0f * Width)}, {0x110, std::bit_cast(2.0f * Width)}, + {0x111, std::bit_cast(2.0f * Height)}, {0x112, std::bit_cast(2.0f * Height)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + {0x318, static_cast(target >> 8u)}, {0x390, static_cast(target >> 40u)}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 1u << 1u}, {0x8c, 0}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}, + }; + Append(commands, 0x79, {0x242, 4}); + Append(commands, 0x79, {0x24a, 0}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + Append(commands, 0x2d, {3, 2}); +} + +void AppendGate(std::vector& commands) { + const auto gate = reinterpret_cast(&Gate); + commands.insert(commands.end(), {0xc0053c00u, 0x13u, static_cast(gate), static_cast(gate >> 32u), 1u, 0xffffffffu, 0x19u}); +} + +void Submit(std::vector& commands) { + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); +} + +class Region { +public: + Region(std::size_t bytes, bool watched) : bytes(bytes), arena(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (arena) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + if (data != nullptr) GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (arena) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "linear target completion points: cannot map the guest region"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "linear target completion points: cannot allocate the guest region"); + auto* words = reinterpret_cast(data); + std::fill(words, words + bytes / 4u, Untouched); + if (arena && !watched) { + AgcDriver::GuestMemory::Unwatch(Address(), bytes); + Require(!AgcDriver::GuestMemory::Watched(Address(), bytes), "linear target completion points: the region is still write-watched"); + } + GuestAllocations::Mutation().Add(data, bytes, true, true, true); + } + ~Region() { + GuestAllocations::Mutation().Remove(data); +#ifdef _WIN32 + if (arena) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (arena) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Region(const Region&) = delete; + Region& operator=(const Region&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::uint32_t Word(std::size_t offset) const { return std::atomic_ref(*reinterpret_cast(data + offset)).load(std::memory_order_acquire); } + std::size_t bytes; + +private: + bool arena; + std::uint8_t* data = nullptr; +}; + +void Import(const Region& region) { + const auto address = region.Address(); + const std::vector userData{ + static_cast(address), + static_cast((address >> 32u) & 0xffffu) | (16u << 16u), + static_cast(region.bytes / 16u), + 0x4bfacu, + Untouched, + Untouched, + Untouched, + Untouched, + }; + std::vector commands; + const auto code = reinterpret_cast(FillCode.data()); + Append(commands, 0x76, {0x207, 64, 1, 1}); + Append(commands, 0x76, {0x20c, static_cast(code >> 8u), static_cast(code >> 40u)}); + Append(commands, 0x76, {0x213, (static_cast(userData.size()) << 1u) | 0x80u}); + commands.push_back(0xc0007600u | (static_cast(userData.size()) << 16u)); + commands.push_back(0x240u); + commands.insert(commands.end(), userData.begin(), userData.end()); + commands.insert(commands.end(), {0xc0031500u, static_cast(region.bytes / 1024u), 1, 1, 0x41u}); + Submit(commands); + AgcDriverWaitIdle_nid_postfix(); +} + +bool InsideScissor(std::uint32_t x, std::uint32_t y) { + return x >= Scissor[0] && x < Scissor[2] && y >= Scissor[1] && y < Scissor[3]; +} + +std::string Mismatches(const Region& region) { + std::size_t drawn = 0, outside = 0; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto value = region.Word(y * Pitch + x * 4u); + if (InsideScissor(x, y)) drawn += value == Drawn ? 0u : 1u; + else outside += value == Untouched ? 0u : 1u; + } + } + if (drawn == 0 && outside == 0) return {}; + return std::to_string(drawn) + " of the " + std::to_string((Scissor[2] - Scissor[0]) * (Scissor[3] - Scissor[1])) + " pixels inside the scissor do not hold the draw's color and " + std::to_string(outside) + " pixels outside it changed"; +} + +bool Await(const std::function& done) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (!done()) { + if (std::chrono::steady_clock::now() >= deadline) return false; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return true; +} + +class Request final : public AgcDriver::IFlipRequest { +public: + explicit Request(std::atomic& ready) : ready(ready) {} + void GpuReady(const std::shared_ptr&) override { ++ready; } + void Fail(std::exception_ptr) noexcept override {} +private: + std::atomic& ready; +}; + +class Output final : public AgcDriver::IVideoOutput { +public: + std::shared_ptr Reserve(const AgcDriver::FlipInfo&) override { return std::make_shared(ready); } + void Fail(std::exception_ptr) noexcept override {} + std::atomic ready = 0; +}; + +enum class Point { Flip, Label, Interrupt }; + +struct Events { + KernelEqueue queue = 0; + KernelEqueueRef owner; + int tag = 0; +}; + +std::string Run(Point point, bool watched, const Region& region, Output& output, Events& events) { + const char* name = point == Point::Flip ? "flip" : point == Point::Label ? "label" : "end-of-pipe interrupt"; + const auto target = region.Address(); + Import(region); + Gate.store(0, std::memory_order_release); + std::vector commands; + AppendDraw(commands, target); + if (point == Point::Flip) { + commands.insert(commands.end(), {AgcDriver::FlipPacketHeader, OutputHandle, 0xfffffffeu, 1, 0, 0}); + } else if (point == Point::Label) { + const auto label = target + LabelOffset; + commands.insert(commands.end(), {0xc0064900u, 0x514u, 1u << 29u, static_cast(label), static_cast(label >> 32u), LabelValue, 0, 0}); + } else { + commands.insert(commands.end(), {0xc0064900u, 0x06000528u, 2u << 24u, 0, 0, 0, 0, 0}); + } + AppendGate(commands); + const auto readyBefore = output.ready.load(); + if (point == Point::Interrupt) { + std::array stale{}; + while (events.owner->GetTriggeredEvents(stale.data(), static_cast(stale.size())) > 0) {} + } + Submit(commands); + const bool watchedAtStore = AgcDriver::GuestMemory::Watched(target, TargetBytes); + std::string failure; + if (point == Point::Flip) { + const bool flipped = Await([&] { return output.ready.load() != readyBefore; }); + if (!flipped) failure = "the flip did not reach the GPU"; + else if (!Await([&] { return Mismatches(region).empty(); })) failure = "after the flip, while the queue waited: " + Mismatches(region); + } else if (point == Point::Label) { + if (!Await([&] { return region.Word(LabelOffset) == LabelValue; })) failure = "the label after the draw never landed"; + else if (const auto mismatches = Mismatches(region); !mismatches.empty()) failure = "when the label after the draw landed: " + mismatches; + } else { + std::array delivered{}; + if (!Await([&] { return events.owner->GetTriggeredEvents(delivered.data(), 1) == 1; })) failure = "the end-of-pipe interrupt after the draw was not delivered"; + else if (delivered[0].udata != &events.tag) failure = "an unexpected event was delivered"; + else if (const auto mismatches = Mismatches(region); !mismatches.empty()) failure = "when the end-of-pipe interrupt after the draw was delivered: " + mismatches; + } + Gate.store(1, std::memory_order_release); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::Graphics::StorageTexture::FlushPending(target, region.bytes, nullptr, "test release"); + std::printf("linear target completion point: %s, %s memory (watched at the store: %d): %s\n", name, watched ? "write-watched" : "unwatched", watchedAtStore ? 1 : 0, failure.empty() ? "stored" : failure.c_str()); + if (failure.empty()) return {}; + return std::string(name) + (watched ? " (write-watched memory): " : " (unwatched memory): ") + failure; +} + +} + +int main(int argc, char** argv) { + try { + bool flips = argc == 1, labels = argc == 1, interrupts = argc == 1, watchedFlip = argc == 1; + for (int index = 1; index < argc; ++index) { + const std::string_view argument(argv[index]); + if (argument == "--flip") flips = true; + else if (argument == "--label") labels = true; + else if (argument == "--interrupt") interrupts = true; + else Require(false, "linear target completion points: unknown argument " + std::string(argument)); + } + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + std::vector> regions; + struct ShutdownGuard { + ~ShutdownGuard() { try { AgcDriverShutdown_nid_postfix(); } catch (...) {} } + } shutdown; + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + auto output = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(OutputHandle, output); + Events events; + if (interrupts) { + Require(sceKernelCreateEqueue(&events.queue, "AGC linear target") == 0, "linear target completion points: event queue creation failed"); + events.owner = EqueuePin_nid_postfix(events.queue); + Require(sceAgcDriverAddEqEvent(events.queue, 0, &events.tag) == 0, "linear target completion points: graphics event registration failed"); + } + std::vector failures; + const auto run = [&](Point point, bool watched) { + regions.push_back(std::make_unique(watched ? 16 * BlockBytes : 4 * BlockBytes, watched)); + if (auto failure = Run(point, watched, *regions.back(), *output, events); !failure.empty()) failures.push_back(std::move(failure)); + }; + if (flips) run(Point::Flip, false); + if (watchedFlip) run(Point::Flip, true); + if (labels) run(Point::Label, false); + if (interrupts) run(Point::Interrupt, false); + if (interrupts) { + Require(sceAgcDriverDeleteEqEvent(events.queue, 0) == 0, "linear target completion points: graphics event deletion failed"); + events.owner.reset(); + Require(sceKernelDeleteEqueue(events.queue) == 0, "linear target completion points: event queue deletion failed"); + } + AgcDriverUnregisterVideoOutput_nid_postfix(OutputHandle, output); + if (!failures.empty()) { + std::string message = "a CPU read after a point where the guest can observe a draw's completion did not see the drawn linear target:"; + for (const auto& failure : failures) message += "\n " + failure; + Require(false, message); + } + AgcDriverShutdown_nid_postfix(); + std::puts("linear target completion point tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCompletionStores.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCompletionStores.cpp new file mode 100644 index 0000000000..e6abff0879 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCompletionStores.cpp @@ -0,0 +1,304 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t TargetBytes = std::size_t{Width} * Height * 16u; +constexpr std::size_t LabelOffset = BlockBytes; +constexpr std::uint32_t Rgba32FloatInfo = 0x8738u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32AbgrExport = 9; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 2> Programs{}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +std::span Program(std::size_t index, bool blue) { + auto& code = Programs.at(index); + std::size_t at = 0; + code[at++] = 0x36000000u | (20u << 17u) | 0xffu; + code[at++] = 0x0000ffffu; + code[at++] = 0x2c000000u | (21u << 17u) | 0x90u; + code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 20u); + code[at++] = 0x7e000c00u | (21u << 17u) | (256u + 21u); + code[at++] = (blue ? 0x7e0002f2u : 0x7e000280u) | (22u << 17u); + code[at++] = 0x7e0002f2u | (23u << 17u); + code[at++] = 0xf800180fu; + code[at++] = 0x17161514u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Rgba32FloatInfo; + cx[0x31d] = 0; + cx[0x323] = 0; + cx[0x324] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = LinearAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + color.exportIndex = 0; + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::span pixelCode, VkRect2D scissor) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionFixedPoint); + pixel.targetOutputMode.at(0) = Float32AbgrExport; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + VkPipelineColorBlendAttachmentState blend{}; + blend.blendEnable = VK_FALSE; + blend.colorWriteMask = 0xf; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = scissor; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::array Texel(const std::uint8_t* target, std::uint32_t x, std::uint32_t y) { + std::array texel{}; + std::memcpy(texel.data(), target + (static_cast(y) * Width + x) * 16u, sizeof(texel)); + return texel; +} + +void Label(AgcDriver::VulkanDevice& device, const Block& block, std::uint32_t value, std::uint64_t submissions, bool watched, const char* what) { + const auto bytes = std::as_bytes(std::span(&value, 1)); + const auto reason = device.WriteLabelOnGpu(block.Address() + LabelOffset, bytes, value, 0); + Require(reason == 0, std::string(what) + ": the label was not recorded on the GPU (outcome " + std::to_string(reason) + ")"); + Require(StorageTexture::FindPending(block.Address(), TargetBytes) == nullptr, std::string(what) + ": the drawn linear target was still pending after a label the GPU writes"); + const auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(!watched || (recorder != nullptr && recorder->Submissions() == submissions), std::string(what) + ": storing the drawn linear target before the label waited for recorded work (" + std::to_string(recorder != nullptr ? recorder->Submissions() - submissions : 0) + " submissions)"); +} + +bool Expect(AgcDriver::VulkanDevice& device) { + Block block(16 * BlockBytes); + std::fill(block.data, block.data + block.bytes, std::uint8_t{0xff}); + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if (AgcDriver::Graphics::HostImportFor(context, block.Address(), block.bytes) == nullptr) return false; + const auto color = DecodeTarget(block); + Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && color.bytes == TargetBytes, "the target is not a linear R32G32B32A32 target"); + const auto red = Program(0, false); + const auto blue = Program(1, true); + const auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr, "the device has no recorder"); + const bool watched = AgcDriver::GuestMemory::Watched(block.Address(), TargetBytes); + if (!watched) std::puts("linear target completion stores: the target is outside the write watch, so a store may wait for the previous store's saved bytes; the submission checks are skipped"); + const auto pending = [&](const char* what) { + Require(StorageTexture::FindPending(block.Address(), TargetBytes) != nullptr, std::string(what) + ": the drawn linear target was stored before any point where its completion is observable"); + }; + Draw(device, color, red, {{0, 0}, {Width, Height}}); + pending("after the first draw"); + const auto submissions = recorder->Submissions(); + Draw(device, color, blue, {{0, 0}, {Width / 2u, Height}}); + pending("after the second draw"); + Require(!watched || recorder->Submissions() == submissions, "the second draw into the linear target submitted recorded work"); + Draw(device, color, red, {{Width / 4u, Height / 4u}, {Width / 4u, Height / 2u}}); + pending("after the third draw"); + Require(!watched || recorder->Submissions() == submissions, "the third draw into the linear target submitted recorded work"); + Label(device, block, 1u, submissions, watched, "the first label"); + Draw(device, color, blue, {{Width / 2u, 0}, {Width / 2u, Height}}); + pending("after the fourth draw"); + Label(device, block, 2u, submissions, watched, "the second label"); + Draw(device, color, red, {{0, Height / 2u}, {Width, Height / 2u}}); + pending("after the fifth draw"); + Require(!watched || recorder->Submissions() == submissions, "the fifth draw into the linear target submitted recorded work"); + device.WaitIdle(); + Require(StorageTexture::FindPending(block.Address(), TargetBytes) == nullptr, "the drawn linear target was still pending after the device wait"); + std::vector stored(TargetBytes); + std::memcpy(stored.data(), block.data, TargetBytes); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const bool lastRed = y >= Height / 2u; + const bool lastBlue = !lastRed && x >= Width / 2u; + const bool thirdRed = !lastRed && !lastBlue && x >= Width / 4u && x < Width / 2u && y >= Height / 4u; + const bool secondBlue = !lastRed && !lastBlue && !thirdRed; + const float z = lastRed || thirdRed ? 0.0f : secondBlue || lastBlue ? 1.0f : -1.0f; + const std::array expected{static_cast(x), static_cast(y), z, 1.0f}; + const auto texel = Texel(stored.data(), x, y); + Require(texel == expected, "after the device wait, raw guest read: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds (" + std::to_string(texel[0]) + ", " + std::to_string(texel[1]) + ", " + std::to_string(texel[2]) + ", " + std::to_string(texel[3]) + ")"); + } + } + std::uint32_t label = 0; + std::memcpy(&label, block.data + LabelOffset, sizeof(label)); + Require(label == 2u, "the second label did not land: it holds " + std::to_string(label)); + return true; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!Expect(*device)) { + std::puts("linear target completion store test skipped: no host import on this device"); + return VulkanTestSkipped; + } + std::puts("linear target completion store tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCpuWrites.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCpuWrites.cpp new file mode 100644 index 0000000000..d59d516e00 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCpuWrites.cpp @@ -0,0 +1,299 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t TargetBytes = std::size_t{Width} * Height * 16u; +constexpr std::size_t RowBytes = std::size_t{Width} * 16u; +constexpr std::uint32_t Drawn = Width / 2u; +constexpr std::uint32_t Rgba32FloatInfo = 0x8738u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32AbgrExport = 9; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 2> Programs{}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +std::span Program(std::size_t index, bool blue) { + auto& code = Programs.at(index); + std::size_t at = 0; + code[at++] = 0x36000000u | (20u << 17u) | 0xffu; + code[at++] = 0x0000ffffu; + code[at++] = 0x2c000000u | (21u << 17u) | 0x90u; + code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 20u); + code[at++] = 0x7e000c00u | (21u << 17u) | (256u + 21u); + code[at++] = (blue ? 0x7e0002f2u : 0x7e000280u) | (22u << 17u); + code[at++] = 0x7e0002f2u | (23u << 17u); + code[at++] = 0xf800180fu; + code[at++] = 0x17161514u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +struct Block { + Block(std::size_t bytes, bool stayWatched) : bytes(bytes), arena(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (arena) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (arena) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!arena || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + if (arena && !stayWatched) { + AgcDriver::GuestMemory::Unwatch(Address(), bytes); + Require(!AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is still write-watched"); + } + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (arena) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (arena) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool arena; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Rgba32FloatInfo; + cx[0x31d] = 0; + cx[0x323] = 0; + cx[0x324] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = LinearAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + color.exportIndex = 0; + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::span pixelCode, std::uint32_t scissorWidth) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionFixedPoint); + pixel.targetOutputMode.at(0) = Float32AbgrExport; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + VkPipelineColorBlendAttachmentState blend{}; + blend.blendEnable = VK_FALSE; + blend.colorWriteMask = 0xf; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {scissorWidth, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::array Texel(const std::uint8_t* target, std::uint32_t x, std::uint32_t y) { + std::array texel{}; + std::memcpy(texel.data(), target + static_cast(y) * RowBytes + static_cast(x) * 16u, sizeof(texel)); + return texel; +} + +enum class Outcome { Kept, Lost, Skipped }; + +Outcome Run(AgcDriver::VulkanDevice& device, bool watched, bool seeded) { + const auto name = std::string(seeded ? "seeded" : "unseeded") + ", " + (watched ? "write-watched" : "unwatched") + " memory"; + Block block(watched ? std::size_t{16} * BlockBytes : BlockBytes, watched); + std::fill(block.data, block.data + block.bytes, std::uint8_t{0xff}); + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if (AgcDriver::Graphics::HostImportFor(context, block.Address(), block.bytes) == nullptr) return Outcome::Skipped; + const auto color = DecodeTarget(block); + Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && color.bytes == TargetBytes, "the target is not a linear R32G32B32A32 target"); + Draw(device, color, Program(0, false), Width); + const auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr && !recorder->Idle(), "the draw into an imported linear target waited for the GPU instead of being recorded"); + device.WaitIdle(); + std::vector stored(TargetBytes); + std::memcpy(stored.data(), block.data, TargetBytes); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const std::array expected{static_cast(x), static_cast(y), 0.0f, 1.0f}; + const auto texel = Texel(stored.data(), x, y); + Require(texel == expected, name + ": after the first draw, raw guest read: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds (" + std::to_string(texel[0]) + ", " + std::to_string(texel[1]) + ", " + std::to_string(texel[2]) + ", " + std::to_string(texel[3]) + ")"); + } + } + if (seeded) { + for (std::uint32_t y = 0; y < Height; ++y) std::memset(block.data + y * RowBytes, 0x5a, Drawn * 16u); + } + Draw(device, color, Program(1, true), Drawn); + for (std::uint32_t y = 0; y < Height; ++y) std::memset(block.data + y * RowBytes + Drawn * 16u, 0x42, RowBytes - Drawn * 16u); + const bool watchedAtStore = AgcDriver::GuestMemory::Watched(block.Address(), TargetBytes); + device.WaitIdle(); + std::size_t wrongGpu = 0; + std::size_t wrongCpu = 0; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Drawn; ++x) { + const std::array expected{static_cast(x), static_cast(y), 1.0f, 1.0f}; + if (Texel(block.data, x, y) != expected) ++wrongGpu; + } + auto* begin = block.data + y * RowBytes + Drawn * 16u; + wrongCpu += static_cast(std::count_if(begin, block.data + (y + 1u) * RowBytes, [](std::uint8_t byte) { return byte != 0x42u; })); + } + std::printf("linear target CPU writes, %s (watched at the store: %d): wrong GPU pixels %zu/%u, wrong CPU bytes %zu/%zu\n", name.c_str(), watchedAtStore ? 1 : 0, wrongGpu, Drawn * Height, wrongCpu, (RowBytes - Drawn * 16u) * Height); + return wrongGpu == 0 && wrongCpu == 0 ? Outcome::Kept : Outcome::Lost; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + bool kept = true; + bool ran = false; + for (const bool watched : {true, false}) { + for (const bool seeded : {false, true}) { + const auto outcome = Run(*device, watched, seeded); + ran = ran || outcome != Outcome::Skipped; + kept = kept && outcome != Outcome::Lost; + } + } + if (!ran) { + std::puts("linear target CPU write test skipped: no host import on this device"); + return VulkanTestSkipped; + } + Require(kept, "a drawn linear target's store lost the drawn half or the CPU's later stores to the other half"); + std::puts("linear target CPU write tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetRectangleBound.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetRectangleBound.cpp new file mode 100644 index 0000000000..c913de3925 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetRectangleBound.cpp @@ -0,0 +1,305 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t RegionBytes = 8 * BlockBytes; +constexpr std::uint32_t Rgba32FloatInfo = 0x8738u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32AbgrExport = 9; +constexpr std::uint8_t Initial = 0x3f; +constexpr std::uint8_t Sentinel = 0x37; +constexpr std::uint8_t Gap = 0x29; +constexpr std::size_t GapBytes = 0x100; + +struct Surface { + std::size_t offset; + std::uint32_t width; + std::uint32_t height; + std::size_t RowBytes() const { return std::size_t{width} * 16u; } + std::size_t Bytes() const { return RowBytes() * height; } +}; + +constexpr Surface Tall{0, 64, 256}; +constexpr Surface Shifted{6 * BlockBytes + GapBytes, 64, 16}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 2> Programs{}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +std::span Program(std::size_t index, bool blue) { + auto& code = Programs.at(index); + std::size_t at = 0; + code[at++] = 0x36000000u | (20u << 17u) | 0xffu; + code[at++] = 0x0000ffffu; + code[at++] = 0x2c000000u | (21u << 17u) | 0x90u; + code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 20u); + code[at++] = 0x7e000c00u | (21u << 17u) | (256u + 21u); + code[at++] = (blue ? 0x7e0002f2u : 0x7e000280u) | (22u << 17u); + code[at++] = 0x7e0002f2u | (23u << 17u); + code[at++] = 0xf800180fu; + code[at++] = 0x17161514u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), arena(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (arena) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (arena) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color targets"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color targets"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (arena) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (arena) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool arena; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, const Surface& surface) { + const auto address = block.Address() + surface.offset; + AgcDriver::Registers cx; + cx[0x318] = static_cast(address >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Rgba32FloatInfo; + cx[0x31d] = 0; + cx[0x323] = 0; + cx[0x324] = 0; + cx[0x3b0] = ((surface.width - 1u) << 14u) | (surface.height - 1u); + cx[0x3b8] = LinearAttrib3; + cx[0x390] = static_cast(address >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + color.exportIndex = 0; + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, const Surface& surface, std::span pixelCode, VkRect2D scissor) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionFixedPoint); + pixel.targetOutputMode.at(0) = Float32AbgrExport; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + VkPipelineColorBlendAttachmentState blend{}; + blend.blendEnable = VK_FALSE; + blend.colorWriteMask = 0xf; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {surface.width, surface.height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(surface.height), static_cast(surface.width), -static_cast(surface.height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = scissor; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +bool Inside(const VkRect2D& rect, std::uint32_t x, std::uint32_t y) { + return x >= static_cast(rect.offset.x) && x < static_cast(rect.offset.x) + rect.extent.width && y >= static_cast(rect.offset.y) && y < static_cast(rect.offset.y) + rect.extent.height; +} + +std::uint8_t* Pixel(const Block& block, const Surface& surface, std::uint32_t x, std::uint32_t y) { + return block.data + surface.offset + static_cast(y) * surface.RowBytes() + static_cast(x) * 16u; +} + +bool Holds(const std::uint8_t* pixel, std::uint8_t value) { + return std::all_of(pixel, pixel + 16, [&](std::uint8_t byte) { return byte == value; }); +} + +bool HoldsOutput(const std::uint8_t* pixel, std::uint32_t x, std::uint32_t y, float z) { + std::array texel{}; + std::memcpy(texel.data(), pixel, sizeof(texel)); + return texel == std::array{static_cast(x), static_cast(y), z, 1.0f}; +} + +bool Expect(AgcDriver::VulkanDevice& device) { + Block block(RegionBytes); + std::fill(block.data, block.data + block.bytes, Initial); + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if (AgcDriver::Graphics::HostImportFor(context, block.Address(), block.bytes) == nullptr) return false; + const auto tall = DecodeTarget(block, Tall); + const auto shifted = DecodeTarget(block, Shifted); + Require(tall.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && tall.bytes == Tall.Bytes() && shifted.bytes == Shifted.Bytes(), "the targets are not linear R32G32B32A32 targets"); + constexpr VkRect2D first{{8, 4}, {16, 16}}; + constexpr VkRect2D second{{16, 12}, {24, 16}}; + constexpr VkRect2D whole{{0, 0}, {Shifted.width, Shifted.height}}; + Draw(device, tall, Tall, Program(0, false), first); + Draw(device, tall, Tall, Program(1, true), second); + Draw(device, shifted, Shifted, Program(0, false), whole); + const std::array, 3> sentinels{{{50, 10}, {20, 40}, {30, 200}}}; + for (const auto& [x, y] : sentinels) std::memset(Pixel(block, Tall, x, y), Sentinel, 16); + Require((block.Address() + Shifted.offset) / BlockBytes * BlockBytes == block.Address() + Shifted.offset - GapBytes, "the shifted target does not start past a 64 KiB boundary"); + std::memset(block.data + Shifted.offset - GapBytes, Gap, GapBytes); + device.WaitIdle(); + std::size_t wrongDrawn = 0, wrongSentinels = 0, wrongUntouched = 0, wrongShifted = 0; + for (std::uint32_t y = 0; y < Tall.height; ++y) { + for (std::uint32_t x = 0; x < Tall.width; ++x) { + const auto* pixel = Pixel(block, Tall, x, y); + const bool sentinel = std::any_of(sentinels.begin(), sentinels.end(), [&](const auto& at) { return at[0] == x && at[1] == y; }); + if (sentinel) wrongSentinels += Holds(pixel, Sentinel) ? 0u : 1u; + else if (Inside(second, x, y)) wrongDrawn += HoldsOutput(pixel, x, y, 1.0f) ? 0u : 1u; + else if (Inside(first, x, y)) wrongDrawn += HoldsOutput(pixel, x, y, 0.0f) ? 0u : 1u; + else wrongUntouched += Holds(pixel, Initial) ? 0u : 1u; + } + } + for (std::uint32_t y = 0; y < Shifted.height; ++y) { + for (std::uint32_t x = 0; x < Shifted.width; ++x) wrongShifted += HoldsOutput(Pixel(block, Shifted, x, y), x, y, 0.0f) ? 0u : 1u; + } + const auto gapLost = static_cast(std::count_if(block.data + Shifted.offset - GapBytes, block.data + Shifted.offset, [](std::uint8_t byte) { return byte != Gap; })); + std::printf("linear target rectangle bound: wrong drawn pixels %zu, lost sentinels %zu/%zu, changed untouched pixels %zu, wrong shifted-target pixels %zu, lost gap bytes %zu/%zu\n", wrongDrawn, wrongSentinels, sentinels.size(), wrongUntouched, wrongShifted, gapLost, GapBytes); + Require(wrongDrawn == 0 && wrongShifted == 0, "a drawn linear target's store lost pixels its draws wrote"); + Require(wrongSentinels == 0 && gapLost == 0, "a drawn linear target's store overwrote CPU stores outside the drawn rectangle"); + Require(wrongUntouched == 0, "a drawn linear target's store changed pixels no draw and no CPU store wrote"); + return true; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!Expect(*device)) { + std::puts("linear target rectangle bound test skipped: no host import on this device"); + return VulkanTestSkipped; + } + std::puts("linear target rectangle bound tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 7569ec1c0f..718cb6394e 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -109,7 +109,8 @@ - AMD image descriptors with `TYPE=0` are treated as null for read-only image slots even when the base and other words are nonzero: `image_load` on a gfx1035 with such a T# returns 0,0,0,0, and the same descriptors were seen in direct reads in PPSA01341 on an RTX 4070. Writable and atomic image slots and entries of an image table keep faulting because there is no hardware measurement for them, and the reserved types 2-7 also fault. `TYPE=1` is treated as null in the same read-only slots: gfx1035 reads it as zeros too, and gfx1010 returns zeros for `image_sample_lz`, `image_sample_b`, `image_load` and `image_get_resinfo` with format 22 (#5828); it is not measured on RDNA2, with other formats or with depth-compare reads (`image_sample_c*`). gfx1013 behavior is unverified - On a device without VK_EXT_robustness2's `nullDescriptor`, such as MoltenVK, a null image descriptor in a typed heap is a [placeholder image](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp) of that heap's type, 1×1 and cleared to zero, which every null descriptor of the heap shares. Reads give zeros, as `nullDescriptor` allows, but stores and atomics to it are kept, so a later read of a null storage image can return them, and a depth comparison compares against depth 0. A heap whose placeholder the device cannot create, for example 1D or 3D depth, 64-bit atomics without 64-bit image atomics, or multisampled storage without `shaderStorageImageMultisample`, throws when a null descriptor reaches it. - [Draw input snapshots](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`. -- [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. Explicit-size file writes (`write`, `pwrite`, `writev`, `pwritev`, AIO and `fwrite`) synchronize their source ranges through the [host-read hook](../../../core/libs/prx/libc/src/GuestArena.cpp); other host readers are not covered. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default. +- [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver), except drawn resident linear targets (next entry), are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. Explicit-size file writes (`write`, `pwrite`, `writev`, `pwritev`, AIO and `fwrite`) synchronize their source ranges through the [host-read hook](../../../core/libs/prx/libc/src/GuestArena.cpp); other host readers are not covered. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default. +- [Resident linear color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) written by recorded draws are stored before the graphics queue's next flip, label, end-of-pipe interrupt, drain or device wait instead of when the draws complete, and each store copies only the union of the draws' scissor rectangles since the last store. A CPU store made before that point inside the rectangle, on a pixel no primitive wrote, is overwritten by the store, where the console keeps it; CPU stores outside the rectangle stay, whether or not the write watch saw them. A target with an attachment proxy, several mips or layers, a 3D or DCC-compressed surface, or results of a dispatch, fill or copy is stored whole, and its CPU-written 64 KiB blocks then get the GPU's bytes. A label the draw pipeline commits does not store a target whose range a queued draw still reads (`DrawPipeline::HeldReads`). - [`APS5_SKIP_REFUSED_DRAWS`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp) (libSceAgcDriver) drops a draw whose graphics stage the recompiler refuses instead of failing the queue, so the frame lacks that draw, and the stage is compiled and refused again at each such draw. Compute refusals still fail. - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending saves its bytes through the unwatch hook, which also runs when a failed collect takes a range out of the watch. A range that leaves the watch without the hook running over it (an import fallback that unwatched without saving) leaves its pending results without saved bytes; storing such results throws. A unit whose results were stored while its blocks were watched keeps only a generation; when its blocks leave the watch while nothing is pending, the hook saves nothing, and results that land in it afterwards have neither saved bytes nor a watch, so storing them throws too. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory when the import probe keeps the write watch and a settle queue exists: those of 1 MiB or more stay watched, and smaller ones return to the watch once their settle has completed, unless a storage image overlaps them (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. From 24c55f0d3ed96f432994c87bbf7e97a349b5a9f2 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Thu, 22 Oct 2026 15:16:56 +0700 Subject: [PATCH 114/428] fix(agc): store drawn linear targets before a draw or dispatch that touches their memory With the store deferred to completion points, a draw or dispatch that reads a drawn linear target's guest memory through an address, or writes it through a buffer, saw the bytes from before the draw that rendered it, and the later rectangle store would land over its writes. agc_driver_address_passes caught the read: a draw reading through the address table the linear target the previous draw rendered read the bytes before it, where the per-draw store had stored them first. Before a recorded draw and a dispatch, the drawn targets whose memory the work reads in place or writes are stored, and all of them for address-based work, whose memory is unknown. Co-Authored-By: Claude --- .../Execution/src/VulkanDevice.cpp | 1 + .../Graphics/include/Texture.hpp | 2 ++ .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 1 + .../libSceAgcDriver/Graphics/src/Texture.cpp | 24 +++++++++++++++++++ 4 files changed, 28 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 2ac6b80b04..47fb6f103b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -4850,6 +4850,7 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { }; if (recorder.HasQueuedKeyStores() && (resources.HoldsLease() || recorder.AnyQueuedKeyStore(touches))) recorder.FlushKeyStores(); if (recorder.HasQueuedStores() && (resources.HoldsLease() || recorder.AnyQueuedStore(touches))) recorder.FlushStores(); + static_cast(Graphics::StorageTexture::StoreDrawnLinearTouching("dispatch access", touches, resources.HoldsLease())); VkAccessFlags covered = 0; const auto commands = recorder.Commands(&covered); recordStep(PhaseRecordCommands); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index aa4cc1d36c..504296a488 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -221,6 +222,7 @@ public: static std::size_t StoreSmallPending(const char* reason); static void StoreReadbackBeforeLabel(const char* reason); static std::size_t StoreDrawnLinear(const char* reason); + static std::size_t StoreDrawnLinearTouching(const char* reason, const std::function& touches, bool all); static bool AnyPending(); static void FlushDumpSelected(const char* reason); static bool StoreAtFlipRequested(const char* value); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 005e697446..090b9994a7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1973,6 +1973,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam } for (const auto image : record.multisampleImages) passAttachments.emplace_back(image, true); const bool unknownMemory = addressBased || resources.UsesAddressTable() || resources.HasCopiedWrites(); + static_cast(StorageTexture::StoreDrawnLinearTouching("draw access", [&](std::uint64_t begin, std::uint64_t end) { return std::any_of(passReads.begin(), passReads.end(), [&](const auto& read) { return read.first < end && begin < read.second; }) || resources.WritesOverlap(begin, static_cast(end - begin)); }, unknownMemory)); static const bool addressReadPasses = std::getenv("APS5_NO_ADDRESS_READ_PASSES") == nullptr; const bool readOnlyBda = addressReadPasses && resources.BdaReadOnly(); outcome.addressDraw = addressBased; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index f9a5171189..0367383068 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -4495,6 +4495,30 @@ std::size_t StorageTexture::StoreDrawnLinear(const char* reason) { return stored; } +std::size_t StorageTexture::StoreDrawnLinearTouching(const char* reason, const std::function& touches, bool all) { + auto& pending = Pending(); + if (pending.drawnCount.load(std::memory_order_acquire) == 0 || Recorder::InCompletion()) return 0; + std::vector> ranges; + { + std::lock_guard lock(pending.mutex); + for (const auto& entry : pending.drawn) { + if (!entry.texture->dirty) continue; + const auto begin = entry.texture->descriptor.baseAddress; + const auto end = begin + entry.texture->guestBytes; + if (!all && !touches(begin, end)) continue; + ranges.emplace_back(begin, static_cast(entry.texture->guestBytes)); + } + } + if (ranges.empty()) return 0; + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); + std::lock_guard gpu(GuestMemory::GpuMutex()); + std::size_t stored = 0; + for (const auto& [address, bytes] : ranges) { + if (FlushPending(address, bytes, nullptr, reason)) ++stored; + } + return stored; +} + bool StorageTexture::AnyPending() { auto& pending = Pending(); std::lock_guard lock(pending.mutex); From c7f9eaa30ed74deb65fd314dc2d9edce5f589517 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Thu, 22 Oct 2026 15:55:35 +0700 Subject: [PATCH 115/428] fix(agc): close two ordering gaps of the drawn linear target stores - A flush from a thread without the GPU lock takes the lock before it takes a drawn target out of the pending set, so a flip, label or interrupt on the queue worker cannot run between the two and get ahead of the target's store. - A label written on the GPU stores the drawn targets first and submits that store with the label, so the label's end-of-pipe interrupt is not held back to a later submission. - The pending-writer check skips the target's own earlier stores only when the flush stores that target alone. Co-Authored-By: Claude --- .../Execution/src/Driver/Packets/PacketMemory.cpp | 2 ++ .../prx/libSceAgcDriver/Graphics/src/Texture.cpp | 15 ++++++++++++++- docs/dev/debt/libSceAgcDriver.md | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp index a20b86753f..53cb9663d0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -43,6 +43,7 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue if (label.has_value()) { const auto bytes = label->Bytes(); const auto stamp = ++eventSerial; + if (localDevice != nullptr && submission.queue == 0 && Graphics::StorageTexture::StoreDrawnLinear("label") != 0) workOpen = true; reason = localDevice != nullptr ? localDevice->WriteLabelOnGpu(label->address, bytes, stamp, submission.queue) : 4; if (reason == 1) GuestMemory::Write(label->address, bytes, 4); if (reason == 0 || reason == 1 || reason == 5) { @@ -444,6 +445,7 @@ void Driver::commitLabel(std::uint32_t queue, std::uint64_t address, std::spanWriteLabelOnGpu(address, bytes, stamp, queue) : 4; if (reason != 0 && reason != 5 && reason != 6) { if (reason != 1 && localDevice != nullptr) localDevice->WaitIdle(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 0367383068..d6b3d3e6c8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -4131,6 +4131,18 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons queuedReadHeld.fetch_add(1, std::memory_order_relaxed); return false; } + std::unique_lock drawnHold; + if (auto& drawnPending = Pending(); drawnPending.drawnCount.load(std::memory_order_acquire) != 0 && !GuestMemory::GpuMutex().HeldByThisThread()) { + bool overlap = false; + { + std::lock_guard lock(drawnPending.mutex); + overlap = std::any_of(drawnPending.drawn.begin(), drawnPending.drawn.end(), [&](const PendingWrites::Drawn& entry) { return entry.texture->dirty && entry.texture->overlaps(address, bytes); }); + } + if (overlap) { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); + drawnHold = std::unique_lock(GuestMemory::GpuMutex()); + } + } // The images stay alive across the scan: their last owner may be a batch's kept list, which // another thread releases outside the GPU mutex once the batch completed. std::vector> flush; @@ -4167,6 +4179,7 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons } } if (flush.empty()) { + if (drawnHold.owns_lock()) drawnHold.unlock(); const bool stored = WaitForFlushing(address, bytes, except); // Units shadowed over the range (another image's results retiled into the import's unit // shadow, or an earlier flush's) still reach the import as the scope asks. @@ -4190,7 +4203,7 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons for (const auto& texture : flush) std::fprintf(stderr, "[flush] image 0x%llx+0x%llx for %s 0x%llx+0x%zx\n", static_cast(texture->descriptor.baseAddress), static_cast(texture->guestBytes), reason, static_cast(address), bytes); } std::lock_guard gpu(GuestMemory::GpuMutex()); - if (!ownNotes.empty() && std::any_of(flush.begin(), flush.end(), [](const std::shared_ptr& texture) { return ExistingHostImportFor(texture->context, texture->descriptor.baseAddress, static_cast(texture->guestBytes)) == nullptr; })) ownNotes.clear(); + if (!ownNotes.empty() && (flush.size() != 1 || std::any_of(flush.begin(), flush.end(), [](const std::shared_ptr& texture) { return ExistingHostImportFor(texture->context, texture->descriptor.baseAddress, static_cast(texture->guestBytes)) == nullptr; }))) ownNotes.clear(); if (auto* recorder = Recorder::Active(); recorder != nullptr && !Recorder::InCompletion() && recorder->PendingWriterSerial(address, bytes, ownNotes) != 0) { try { recorder->Reap(); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 718cb6394e..54780e9324 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -110,7 +110,7 @@ - On a device without VK_EXT_robustness2's `nullDescriptor`, such as MoltenVK, a null image descriptor in a typed heap is a [placeholder image](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp) of that heap's type, 1×1 and cleared to zero, which every null descriptor of the heap shares. Reads give zeros, as `nullDescriptor` allows, but stores and atomics to it are kept, so a later read of a null storage image can return them, and a depth comparison compares against depth 0. A heap whose placeholder the device cannot create, for example 1D or 3D depth, 64-bit atomics without 64-bit image atomics, or multisampled storage without `shaderStorageImageMultisample`, throws when a null descriptor reaches it. - [Draw input snapshots](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`. - [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver), except drawn resident linear targets (next entry), are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. Explicit-size file writes (`write`, `pwrite`, `writev`, `pwritev`, AIO and `fwrite`) synchronize their source ranges through the [host-read hook](../../../core/libs/prx/libc/src/GuestArena.cpp); other host readers are not covered. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default. -- [Resident linear color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) written by recorded draws are stored before the graphics queue's next flip, label, end-of-pipe interrupt, drain or device wait instead of when the draws complete, and each store copies only the union of the draws' scissor rectangles since the last store. A CPU store made before that point inside the rectangle, on a pixel no primitive wrote, is overwritten by the store, where the console keeps it; CPU stores outside the rectangle stay, whether or not the write watch saw them. A target with an attachment proxy, several mips or layers, a 3D or DCC-compressed surface, or results of a dispatch, fill or copy is stored whole, and its CPU-written 64 KiB blocks then get the GPU's bytes. A label the draw pipeline commits does not store a target whose range a queued draw still reads (`DrawPipeline::HeldReads`). +- [Resident linear color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) written by recorded draws are stored before the graphics queue's next flip, label, end-of-pipe interrupt, drain or device wait instead of when the draws complete, and each store copies only the union of the draws' scissor rectangles since the last store. A CPU store made before that point inside the rectangle, on a pixel no primitive wrote, is overwritten by the store, where the console keeps it; CPU stores outside the rectangle stay, whether or not the write watch saw them. A target with an attachment proxy, several mips or layers, a 3D or DCC-compressed surface, or results of a dispatch, fill or copy is stored whole, and its CPU-written 64 KiB blocks then get the GPU's bytes. A store the draw pipeline's committer makes (for a label, an end-of-pipe interrupt or a device wait it commits) skips a target whose range a queued draw still reads (`DrawPipeline::HeldReads`). - [`APS5_SKIP_REFUSED_DRAWS`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp) (libSceAgcDriver) drops a draw whose graphics stage the recompiler refuses instead of failing the queue, so the frame lacks that draw, and the stage is compiled and refused again at each such draw. Compute refusals still fail. - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending saves its bytes through the unwatch hook, which also runs when a failed collect takes a range out of the watch. A range that leaves the watch without the hook running over it (an import fallback that unwatched without saving) leaves its pending results without saved bytes; storing such results throws. A unit whose results were stored while its blocks were watched keeps only a generation; when its blocks leave the watch while nothing is pending, the hook saves nothing, and results that land in it afterwards have neither saved bytes nor a watch, so storing them throws too. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory when the import probe keeps the write watch and a settle queue exists: those of 1 MiB or more stay watched, and smaller ones return to the watch once their settle has completed, unless a storage image overlaps them (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. From 24e2c283918c3c33f33d4ed0c487c9ef1266faeb Mon Sep 17 00:00:00 2001 From: thuanquang Date: Thu, 22 Oct 2026 16:34:23 +0700 Subject: [PATCH 116/428] fix(agc): keep drawn linear target results through flush races and CPU-kept hooks - MarkDrawn registers the scissor while any layer is pending instead of while the image is dirty. A flush on another thread can take the image out of the pending set between MarkSliceDirty and the registration; layerPending changes only under the GPU lock, so the flusher, which reads the drawn area after taking that lock, stores the widened area. - AccessKeptByCpu no longer skips or evicts a drawn target. Its store writes CPU-stamped blocks inside the drawn rectangle, so the hook's CPU-kept rule would drop the draw's results. - A drawn target that is stored whole keeps main's rule that CPU-written 64 KiB blocks stay the CPU's. - A linear target that is no longer cached is stored after its draw, as before: the pending registry holds raw pointers. - LinearTargetCompletionPoints skips without host import, where the target is never resident. LinearTargetCpuWrites adds a driver read of the target between the CPU stores and the store. Co-Authored-By: Claude --- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 4 ++++ .../libSceAgcDriver/Graphics/src/Texture.cpp | 5 +++-- .../LinearTargetCompletionPoints.cpp | 13 ++++++++++++ .../tests/execution/LinearTargetCpuWrites.cpp | 21 ++++++++++++++----- docs/dev/debt/libSceAgcDriver.md | 2 +- 5 files changed, 37 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 090b9994a7..903b386b3e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1728,6 +1728,10 @@ void keepRecordedDraw(Recorder& recorder, const std::shared_ptr else if (index < slices.size()) residents[index]->MarkSliceDirty(slices[index].first, slices[index].second); else residents[index]->MarkDirty(); } + for (const auto& resident : residents) { + if (resident == nullptr || resident->Descriptor().tileMode != TextureTileMode::kLinear || resident->Cached()) continue; + StorageTexture::FlushPending(resident->Descriptor().baseAddress, static_cast(resident->GuestBytes()), nullptr, "linear target"); + } } // What recordDraw records from: the recorder, the resources, the pipeline and framebuffer, the diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index d6b3d3e6c8..bea6bd4512 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3914,7 +3914,7 @@ void StorageTexture::MarkDrawn(std::uint32_t slice, std::uint32_t count, VkRect2 const bool rectangle = proxyImage == VK_NULL_HANDLE && descriptor.tileMode == TextureTileMode::kLinear && !blockUnits && trackedLayers == 1 && descriptor.mipCount == 1 && arrayLayers == 1 && geometry.imageLayers == 1 && geometry.imageDepth == 1 && !geometry.thick && descriptor.dccAddress == 0; auto& pending = Pending(); std::lock_guard lock(pending.mutex); - if (!dirty) return; + if (!anyLayerPending()) return; if (!drawnListed) { drawnListed = true; drawnWhole = false; @@ -4708,6 +4708,7 @@ bool StorageTexture::AccessKeptByCpu(std::uint64_t address, std::size_t bytes, s // store there would see, and the stamps only grow. GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Hook); std::lock_guard gpu(GuestMemory::GpuMutex()); + if (std::any_of(overlapping.begin(), overlapping.end(), [](const std::shared_ptr& texture) { return texture->drawnArea().drawn; })) return false; if (!blocksKept(overlapping, address, bytes)) return false; const auto site = static_cast(GuestMemory::CurrentReadSite()); std::size_t dropped = 0; @@ -5789,7 +5790,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { const auto index = static_cast((at - spanBegin) / block); const bool kept = keptAtUnwatchHolds(from, layer); const bool written = kept || unknown || (changedBlocks[index] == GuestMemory::BlockWritten && (compared[layer] || !edge || GuestMemory::StoredOver(from, static_cast(to - from), layerGeneration[layer])) && !unchangedSinceBaseline(from, to)); - if (written && (!drawn.drawn || (tracked && !kept && !unknown && cpuBlocks[index] == 0))) { + if (written && (!drawn.rect || (tracked && !kept && !unknown && cpuBlocks[index] == 0))) { skippedAny = true; skippedLayer[layer] = true; ++skipped; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCompletionPoints.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCompletionPoints.cpp index 558b04df4b..28c1d9aecc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCompletionPoints.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCompletionPoints.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" #include "prx/libSceAgcDriver/Eq/include/Event.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" @@ -41,6 +42,14 @@ extern "C" int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name); extern "C" int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq); +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + namespace { using AgcDriver::Graphics::Require; @@ -338,6 +347,10 @@ int main(int argc, char** argv) { { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + if (AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).hostImportAlignment == 0) { + std::puts("linear target completion point tests skipped: no host import on this device, so the target is never resident"); + return VulkanTestSkipped; + } } std::vector> regions; struct ShutdownGuard { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCpuWrites.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCpuWrites.cpp index d59d516e00..cbd2e525e4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCpuWrites.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCpuWrites.cpp @@ -228,8 +228,8 @@ std::array Texel(const std::uint8_t* target, std::uint32_t x, std::uin enum class Outcome { Kept, Lost, Skipped }; -Outcome Run(AgcDriver::VulkanDevice& device, bool watched, bool seeded) { - const auto name = std::string(seeded ? "seeded" : "unseeded") + ", " + (watched ? "write-watched" : "unwatched") + " memory"; +Outcome Run(AgcDriver::VulkanDevice& device, bool watched, bool seeded, bool hookRead) { + const auto name = std::string(seeded ? "seeded" : "unseeded") + ", " + (watched ? "write-watched" : "unwatched") + " memory" + (hookRead ? ", driver read before the store" : ""); Block block(watched ? std::size_t{16} * BlockBytes : BlockBytes, watched); std::fill(block.data, block.data + block.bytes, std::uint8_t{0xff}); const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); @@ -255,6 +255,15 @@ Outcome Run(AgcDriver::VulkanDevice& device, bool watched, bool seeded) { Draw(device, color, Program(1, true), Drawn); for (std::uint32_t y = 0; y < Height; ++y) std::memset(block.data + y * RowBytes + Drawn * 16u, 0x42, RowBytes - Drawn * 16u); const bool watchedAtStore = AgcDriver::GuestMemory::Watched(block.Address(), TargetBytes); + if (hookRead) { + std::vector row(RowBytes); + AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(row)), 1); + for (std::uint32_t x = 0; x < Drawn; ++x) { + const std::array expected{static_cast(x), 0.0f, 1.0f, 1.0f}; + const auto texel = Texel(row.data(), x, 0); + Require(texel == expected, name + ": a driver read of row 0 before the store: pixel (" + std::to_string(x) + ", 0) holds (" + std::to_string(texel[0]) + ", " + std::to_string(texel[1]) + ", " + std::to_string(texel[2]) + ", " + std::to_string(texel[3]) + "), not the second draw's"); + } + } device.WaitIdle(); std::size_t wrongGpu = 0; std::size_t wrongCpu = 0; @@ -280,9 +289,11 @@ int main() { bool ran = false; for (const bool watched : {true, false}) { for (const bool seeded : {false, true}) { - const auto outcome = Run(*device, watched, seeded); - ran = ran || outcome != Outcome::Skipped; - kept = kept && outcome != Outcome::Lost; + for (const bool hookRead : {false, true}) { + const auto outcome = Run(*device, watched, seeded, hookRead); + ran = ran || outcome != Outcome::Skipped; + kept = kept && outcome != Outcome::Lost; + } } } if (!ran) { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 54780e9324..afb0ef135d 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -110,7 +110,7 @@ - On a device without VK_EXT_robustness2's `nullDescriptor`, such as MoltenVK, a null image descriptor in a typed heap is a [placeholder image](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp) of that heap's type, 1×1 and cleared to zero, which every null descriptor of the heap shares. Reads give zeros, as `nullDescriptor` allows, but stores and atomics to it are kept, so a later read of a null storage image can return them, and a depth comparison compares against depth 0. A heap whose placeholder the device cannot create, for example 1D or 3D depth, 64-bit atomics without 64-bit image atomics, or multisampled storage without `shaderStorageImageMultisample`, throws when a null descriptor reaches it. - [Draw input snapshots](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`. - [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver), except drawn resident linear targets (next entry), are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. Explicit-size file writes (`write`, `pwrite`, `writev`, `pwritev`, AIO and `fwrite`) synchronize their source ranges through the [host-read hook](../../../core/libs/prx/libc/src/GuestArena.cpp); other host readers are not covered. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default. -- [Resident linear color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) written by recorded draws are stored before the graphics queue's next flip, label, end-of-pipe interrupt, drain or device wait instead of when the draws complete, and each store copies only the union of the draws' scissor rectangles since the last store. A CPU store made before that point inside the rectangle, on a pixel no primitive wrote, is overwritten by the store, where the console keeps it; CPU stores outside the rectangle stay, whether or not the write watch saw them. A target with an attachment proxy, several mips or layers, a 3D or DCC-compressed surface, or results of a dispatch, fill or copy is stored whole, and its CPU-written 64 KiB blocks then get the GPU's bytes. A store the draw pipeline's committer makes (for a label, an end-of-pipe interrupt or a device wait it commits) skips a target whose range a queued draw still reads (`DrawPipeline::HeldReads`). +- [Resident linear color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) written by recorded draws are stored before the graphics queue's next flip, label, end-of-pipe interrupt, drain or device wait instead of when the draws complete, and each store copies only the union of the draws' scissor rectangles since the last store. A CPU store made before that point inside the rectangle, on a pixel no primitive wrote, is overwritten by the store, where the console keeps it; CPU stores outside the rectangle stay, whether or not the write watch saw them. A target with an attachment proxy, several mips or layers, a 3D or DCC-compressed surface, or results of a dispatch, fill or copy is stored whole and keeps main's rule that CPU-written 64 KiB blocks stay the CPU's, so GPU results in such a block are dropped. A store the draw pipeline's committer makes (for a label, an end-of-pipe interrupt or a device wait it commits) skips a target whose range a queued draw still reads (`DrawPipeline::HeldReads`). - [`APS5_SKIP_REFUSED_DRAWS`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp) (libSceAgcDriver) drops a draw whose graphics stage the recompiler refuses instead of failing the queue, so the frame lacks that draw, and the stage is compiled and refused again at each such draw. Compute refusals still fail. - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending saves its bytes through the unwatch hook, which also runs when a failed collect takes a range out of the watch. A range that leaves the watch without the hook running over it (an import fallback that unwatched without saving) leaves its pending results without saved bytes; storing such results throws. A unit whose results were stored while its blocks were watched keeps only a generation; when its blocks leave the watch while nothing is pending, the hook saves nothing, and results that land in it afterwards have neither saved bytes nor a watch, so storing them throws too. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory when the import probe keeps the write watch and a settle queue exists: those of 1 MiB or more stay watched, and smaller ones return to the watch once their settle has completed, unless a storage image overlaps them (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. From 14d263c4166d9927d1b689e22363d96427e11363 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Thu, 22 Oct 2026 17:14:53 +0700 Subject: [PATCH 117/428] fix(agc): store the exact union of a linear target's drawn rectangles A drawn target kept the bounding box of its draws' scissors, so a store at a completion point also wrote the pixels between two disjoint scissors, over CPU stores made there after the draws. - The target keeps the clipped scissor rectangles. Two merge only when their union is a rectangle: one inside the other, or the same columns overlapping or touching in rows, or the same rows overlapping or touching in columns. - The store still copies the rectangles' bounding box out of the image, but writes to guest memory, for each row, only the union of the column runs of the rectangles that cover that row. - At most 16 rectangles wait for a completion point: the draw that brings a 17th stores the target right after it, with all of them. - A target the rectangle store does not cover (attachment proxy, several mips or layers, 3D or DCC surface, results of a dispatch, fill or copy) is stored after each draw again, instead of whole at the next completion point. - LinearTargetRectangleBound draws and waits once before its draws, so the target's upload runs before the CPU stores instead of in the same batch, adds CPU stores between the two scissors, and adds a case with 17 one-row scissors and a CPU store between two of them. Co-Authored-By: Claude --- .../Graphics/include/Texture.hpp | 15 ++-- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 8 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 79 +++++++++++++------ .../execution/LinearTargetRectangleBound.cpp | 53 +++++++++++-- docs/dev/debt/libSceAgcDriver.md | 2 +- 5 files changed, 116 insertions(+), 41 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 504296a488..360430b517 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -204,7 +204,7 @@ public: bool BlockUnits() const { return blockUnits; } std::uint32_t TrackedUnits() const { return trackedLayers; } void MarkSliceDirty(std::uint32_t slice, std::uint32_t count = 1); - void MarkDrawn(std::uint32_t slice, std::uint32_t count, VkRect2D scissor); + bool MarkDrawn(std::uint32_t slice, std::uint32_t count, VkRect2D scissor); void Flush(); bool Dirty() const; // Stores every pending image overlapping the range, except `except`; returns whether any was. @@ -484,6 +484,12 @@ private: // (`shadowed` receives those guest ranges, `imported` the pieces written to the import). std::uint64_t uploadWindows(const HostImport& import, std::span> runs, bool discard = false); std::uint64_t writeBackWindows(std::span segments, std::span> keep, std::uint64_t firstStored, std::uint64_t lastStored, std::vector& shadowed, std::vector>& imported, Buffer* save = nullptr); + struct DrawnRect { + std::uint32_t left = 0; + std::uint32_t top = 0; + std::uint32_t right = 0; + std::uint32_t bottom = 0; + }; struct DrawnArea { bool drawn = false; bool rect = false; @@ -491,10 +497,12 @@ private: std::uint32_t top = 0; std::uint32_t right = 0; std::uint32_t bottom = 0; + std::vector rects; }; DrawnArea drawnArea() const; std::vector> drawnSpans(const DrawnArea& area, std::span> keep) const; std::uint64_t storeDrawnRect(const DrawnArea& area, std::span segments, std::span> stores, const std::vector& saveUnits); + void addDrawnRect(DrawnRect rect); void forgetDrawn(); void noteStore(std::uint64_t note); // Per-layer validity (thin array surfaces whose layers are 64 KiB-aligned guest slices; every @@ -660,10 +668,7 @@ private: bool drawnWhole = false; bool drawingTarget = false; bool pendingFromOthers = false; - std::uint32_t drawnLeft = 0; - std::uint32_t drawnTop = 0; - std::uint32_t drawnRight = 0; - std::uint32_t drawnBottom = 0; + std::vector drawnRects; std::vector storeNotes; std::uint64_t version = 0; std::uint64_t writes = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 903b386b3e..09fa6e7079 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1722,15 +1722,15 @@ void keepRecordedDraw(Recorder& recorder, const std::shared_ptr if (resources->Reusable()) recorder.OnComplete([resources] { resources->CheckFaults(); }); else recorder.OnComplete([resources] { resources->WriteBackBuffers(); }); } + std::vector storeNow(residents.size(), false); for (std::size_t index = 0; index < residents.size(); ++index) { if (residents[index] == nullptr) continue; - if (residents[index]->Descriptor().tileMode == TextureTileMode::kLinear) residents[index]->MarkDrawn(index < slices.size() ? slices[index].first : 0u, index < slices.size() ? slices[index].second : 0u, scissor); + if (residents[index]->Descriptor().tileMode == TextureTileMode::kLinear) storeNow[index] = !residents[index]->MarkDrawn(index < slices.size() ? slices[index].first : 0u, index < slices.size() ? slices[index].second : 0u, scissor) || !residents[index]->Cached(); else if (index < slices.size()) residents[index]->MarkSliceDirty(slices[index].first, slices[index].second); else residents[index]->MarkDirty(); } - for (const auto& resident : residents) { - if (resident == nullptr || resident->Descriptor().tileMode != TextureTileMode::kLinear || resident->Cached()) continue; - StorageTexture::FlushPending(resident->Descriptor().baseAddress, static_cast(resident->GuestBytes()), nullptr, "linear target"); + for (std::size_t index = 0; index < residents.size(); ++index) { + if (storeNow[index]) StorageTexture::FlushPending(residents[index]->Descriptor().baseAddress, static_cast(residents[index]->GuestBytes()), nullptr, "linear target"); } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index bea6bd4512..69ad62fa9e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3899,7 +3899,9 @@ bool StorageTexture::Dirty() const { return dirty; } -void StorageTexture::MarkDrawn(std::uint32_t slice, std::uint32_t count, VkRect2D scissor) { +constexpr std::size_t MaxDrawnRects = 16; + +bool StorageTexture::MarkDrawn(std::uint32_t slice, std::uint32_t count, VkRect2D scissor) { struct Drawing { bool& flag; bool previous; @@ -3914,27 +3916,34 @@ void StorageTexture::MarkDrawn(std::uint32_t slice, std::uint32_t count, VkRect2 const bool rectangle = proxyImage == VK_NULL_HANDLE && descriptor.tileMode == TextureTileMode::kLinear && !blockUnits && trackedLayers == 1 && descriptor.mipCount == 1 && arrayLayers == 1 && geometry.imageLayers == 1 && geometry.imageDepth == 1 && !geometry.thick && descriptor.dccAddress == 0; auto& pending = Pending(); std::lock_guard lock(pending.mutex); - if (!anyLayerPending()) return; + if (!anyLayerPending()) return true; if (!drawnListed) { drawnListed = true; drawnWhole = false; - drawnLeft = drawnTop = drawnRight = drawnBottom = 0; + drawnRects.clear(); pending.drawn.push_back({this, guestBytes, std::chrono::steady_clock::now()}); pending.drawnCount.store(pending.drawn.size(), std::memory_order_release); } if (!rectangle || pendingFromOthers) drawnWhole = true; - if (drawnWhole || right <= left || bottom <= top) return; - if (drawnRight <= drawnLeft || drawnBottom <= drawnTop) { - drawnLeft = left; - drawnTop = top; - drawnRight = right; - drawnBottom = bottom; - return; + if (!drawnWhole && right > left && bottom > top) addDrawnRect({left, top, right, bottom}); + return !drawnWhole && drawnRects.size() <= MaxDrawnRects; +} + +void StorageTexture::addDrawnRect(DrawnRect rect) { + const auto inside = [](const DrawnRect& outer, const DrawnRect& inner) { return outer.left <= inner.left && outer.top <= inner.top && inner.right <= outer.right && inner.bottom <= outer.bottom; }; + if (std::any_of(drawnRects.begin(), drawnRects.end(), [&](const DrawnRect& kept) { return inside(kept, rect); })) return; + for (auto kept = drawnRects.begin(); kept != drawnRects.end();) { + const bool columns = kept->left == rect.left && kept->right == rect.right && kept->top <= rect.bottom && rect.top <= kept->bottom; + const bool rows = kept->top == rect.top && kept->bottom == rect.bottom && kept->left <= rect.right && rect.left <= kept->right; + if (!columns && !rows && !inside(rect, *kept)) { + ++kept; + continue; + } + rect = {std::min(kept->left, rect.left), std::min(kept->top, rect.top), std::max(kept->right, rect.right), std::max(kept->bottom, rect.bottom)}; + drawnRects.erase(kept); + kept = drawnRects.begin(); } - drawnLeft = std::min(drawnLeft, left); - drawnTop = std::min(drawnTop, top); - drawnRight = std::max(drawnRight, right); - drawnBottom = std::max(drawnBottom, bottom); + drawnRects.push_back(rect); } void StorageTexture::forgetDrawn() { @@ -3942,7 +3951,7 @@ void StorageTexture::forgetDrawn() { if (!drawnListed) return; drawnListed = false; drawnWhole = false; - drawnLeft = drawnTop = drawnRight = drawnBottom = 0; + drawnRects.clear(); auto& pending = Pending(); std::erase_if(pending.drawn, [this](const PendingWrites::Drawn& entry) { return entry.texture == this; }); pending.drawnCount.store(pending.drawn.size(), std::memory_order_release); @@ -3960,25 +3969,47 @@ StorageTexture::DrawnArea StorageTexture::drawnArea() const { auto& pending = Pending(); std::lock_guard lock(pending.mutex); if (!drawnListed) return {}; - return {true, !drawnWhole && !pendingFromOthers, drawnLeft, drawnTop, drawnRight, drawnBottom}; + DrawnArea area{true, !drawnWhole && !pendingFromOthers}; + if (!area.rect || drawnRects.empty()) return area; + area.left = area.top = std::numeric_limits::max(); + for (const auto& rect : drawnRects) { + area.left = std::min(area.left, rect.left); + area.top = std::min(area.top, rect.top); + area.right = std::max(area.right, rect.right); + area.bottom = std::max(area.bottom, rect.bottom); + } + area.rects = drawnRects; + return area; } std::vector> StorageTexture::drawnSpans(const DrawnArea& area, std::span> keep) const { std::vector> spans; - if (area.right <= area.left || area.bottom <= area.top) return spans; + if (area.rects.empty()) return spans; const auto& mip = mips.front(); const auto elementBytes = static_cast(BytesPerElement(descriptor.format)); const auto rowBase = descriptor.baseAddress + mip.tiledOffset; + std::vector> columns; std::size_t next = 0; for (auto row = area.top; row < area.bottom; ++row) { + columns.clear(); + for (const auto& rect : area.rects) { + if (rect.top <= row && row < rect.bottom) columns.emplace_back(rect.left, rect.right); + } + std::sort(columns.begin(), columns.end()); const auto rowStart = rowBase + std::uint64_t{row} * mip.pitchBytes; - const auto rowBegin = rowStart + area.left * elementBytes; - const auto rowEnd = rowStart + area.right * elementBytes; - while (next < keep.size() && keep[next].second <= rowBegin) ++next; - for (auto index = next; index < keep.size() && keep[index].first < rowEnd; ++index) { - const auto from = std::max(rowBegin, keep[index].first); - const auto to = std::min(rowEnd, keep[index].second); - if (from < to) spans.emplace_back(from, to); + for (std::size_t first = 0; first < columns.size();) { + auto right = columns[first].second; + auto last = first + 1; + for (; last < columns.size() && columns[last].first <= right; ++last) right = std::max(right, columns[last].second); + const auto rowBegin = rowStart + columns[first].first * elementBytes; + const auto rowEnd = rowStart + right * elementBytes; + first = last; + while (next < keep.size() && keep[next].second <= rowBegin) ++next; + for (auto index = next; index < keep.size() && keep[index].first < rowEnd; ++index) { + const auto from = std::max(rowBegin, keep[index].first); + const auto to = std::min(rowEnd, keep[index].second); + if (from < to) spans.emplace_back(from, to); + } } } return spans; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetRectangleBound.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetRectangleBound.cpp index c913de3925..c19854ea05 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetRectangleBound.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetRectangleBound.cpp @@ -63,6 +63,7 @@ struct Surface { constexpr Surface Tall{0, 64, 256}; constexpr Surface Shifted{6 * BlockBytes + GapBytes, 64, 16}; +constexpr VkRect2D Seed{{63, 255}, {1, 1}}; alignas(256) constexpr std::array VertexCode{ 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, @@ -256,22 +257,27 @@ bool Expect(AgcDriver::VulkanDevice& device) { constexpr VkRect2D first{{8, 4}, {16, 16}}; constexpr VkRect2D second{{16, 12}, {24, 16}}; constexpr VkRect2D whole{{0, 0}, {Shifted.width, Shifted.height}}; + Draw(device, tall, Tall, Program(0, false), Seed); + device.WaitIdle(); Draw(device, tall, Tall, Program(0, false), first); Draw(device, tall, Tall, Program(1, true), second); Draw(device, shifted, Shifted, Program(0, false), whole); const std::array, 3> sentinels{{{50, 10}, {20, 40}, {30, 200}}}; + const std::array, 2> between{{{30, 6}, {10, 25}}}; for (const auto& [x, y] : sentinels) std::memset(Pixel(block, Tall, x, y), Sentinel, 16); + for (const auto& [x, y] : between) std::memset(Pixel(block, Tall, x, y), Sentinel, 16); Require((block.Address() + Shifted.offset) / BlockBytes * BlockBytes == block.Address() + Shifted.offset - GapBytes, "the shifted target does not start past a 64 KiB boundary"); std::memset(block.data + Shifted.offset - GapBytes, Gap, GapBytes); device.WaitIdle(); - std::size_t wrongDrawn = 0, wrongSentinels = 0, wrongUntouched = 0, wrongShifted = 0; + std::size_t wrongDrawn = 0, wrongSentinels = 0, wrongBetween = 0, wrongUntouched = 0, wrongShifted = 0; for (std::uint32_t y = 0; y < Tall.height; ++y) { for (std::uint32_t x = 0; x < Tall.width; ++x) { const auto* pixel = Pixel(block, Tall, x, y); - const bool sentinel = std::any_of(sentinels.begin(), sentinels.end(), [&](const auto& at) { return at[0] == x && at[1] == y; }); - if (sentinel) wrongSentinels += Holds(pixel, Sentinel) ? 0u : 1u; + const auto at = [&](const auto& point) { return point[0] == x && point[1] == y; }; + if (std::any_of(sentinels.begin(), sentinels.end(), at)) wrongSentinels += Holds(pixel, Sentinel) ? 0u : 1u; + else if (std::any_of(between.begin(), between.end(), at)) wrongBetween += Holds(pixel, Sentinel) ? 0u : 1u; else if (Inside(second, x, y)) wrongDrawn += HoldsOutput(pixel, x, y, 1.0f) ? 0u : 1u; - else if (Inside(first, x, y)) wrongDrawn += HoldsOutput(pixel, x, y, 0.0f) ? 0u : 1u; + else if (Inside(first, x, y) || Inside(Seed, x, y)) wrongDrawn += HoldsOutput(pixel, x, y, 0.0f) ? 0u : 1u; else wrongUntouched += Holds(pixel, Initial) ? 0u : 1u; } } @@ -279,20 +285,53 @@ bool Expect(AgcDriver::VulkanDevice& device) { for (std::uint32_t x = 0; x < Shifted.width; ++x) wrongShifted += HoldsOutput(Pixel(block, Shifted, x, y), x, y, 0.0f) ? 0u : 1u; } const auto gapLost = static_cast(std::count_if(block.data + Shifted.offset - GapBytes, block.data + Shifted.offset, [](std::uint8_t byte) { return byte != Gap; })); - std::printf("linear target rectangle bound: wrong drawn pixels %zu, lost sentinels %zu/%zu, changed untouched pixels %zu, wrong shifted-target pixels %zu, lost gap bytes %zu/%zu\n", wrongDrawn, wrongSentinels, sentinels.size(), wrongUntouched, wrongShifted, gapLost, GapBytes); + std::printf("linear target rectangle bound: wrong drawn pixels %zu, lost sentinels %zu/%zu, lost sentinels between the rectangles %zu/%zu, changed untouched pixels %zu, wrong shifted-target pixels %zu, lost gap bytes %zu/%zu\n", wrongDrawn, wrongSentinels, sentinels.size(), wrongBetween, between.size(), wrongUntouched, wrongShifted, gapLost, GapBytes); Require(wrongDrawn == 0 && wrongShifted == 0, "a drawn linear target's store lost pixels its draws wrote"); - Require(wrongSentinels == 0 && gapLost == 0, "a drawn linear target's store overwrote CPU stores outside the drawn rectangle"); + Require(wrongSentinels == 0 && gapLost == 0, "a drawn linear target's store overwrote CPU stores outside the drawn rectangles"); + Require(wrongBetween == 0, "a drawn linear target's store overwrote CPU stores between two draws' scissor rectangles"); Require(wrongUntouched == 0, "a drawn linear target's store changed pixels no draw and no CPU store wrote"); return true; } +bool ExpectManyRows(AgcDriver::VulkanDevice& device) { + Block block(RegionBytes); + std::fill(block.data, block.data + block.bytes, Initial); + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if (AgcDriver::Graphics::HostImportFor(context, block.Address(), block.bytes) == nullptr) return false; + const auto tall = DecodeTarget(block, Tall); + constexpr std::uint32_t Rows = 17; + constexpr std::uint32_t Spacing = 15; + Draw(device, tall, Tall, Program(0, false), Seed); + device.WaitIdle(); + for (std::uint32_t row = 0; row < Rows; ++row) Draw(device, tall, Tall, Program(0, false), VkRect2D{{8, static_cast(row * Spacing)}, {16, 1}}); + constexpr std::array between{10, 70}; + std::memset(Pixel(block, Tall, between[0], between[1]), Sentinel, 16); + device.WaitIdle(); + std::size_t wrongDrawn = 0, wrongUntouched = 0; + for (std::uint32_t y = 0; y < Tall.height; ++y) { + const bool drawnRow = y % Spacing == 0 && y / Spacing < Rows; + for (std::uint32_t x = 0; x < Tall.width; ++x) { + if (x == between[0] && y == between[1]) continue; + const auto* pixel = Pixel(block, Tall, x, y); + if ((drawnRow && x >= 8 && x < 24) || Inside(Seed, x, y)) wrongDrawn += HoldsOutput(pixel, x, y, 0.0f) ? 0u : 1u; + else wrongUntouched += Holds(pixel, Initial) ? 0u : 1u; + } + } + const bool kept = Holds(Pixel(block, Tall, between[0], between[1]), Sentinel); + std::printf("linear target rectangle bound, %u one-row scissors: wrong drawn pixels %zu, sentinel between the rows %s, changed untouched pixels %zu\n", Rows, wrongDrawn, kept ? "kept" : "lost", wrongUntouched); + Require(wrongDrawn == 0, "a drawn linear target's store lost pixels its one-row draws wrote"); + Require(kept, "a drawn linear target's store overwrote a CPU store between two one-row scissors"); + Require(wrongUntouched == 0, "a drawn linear target's store changed pixels no one-row draw and no CPU store wrote"); + return true; +} + } int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - if (!Expect(*device)) { + if (!Expect(*device) || !ExpectManyRows(*device)) { std::puts("linear target rectangle bound test skipped: no host import on this device"); return VulkanTestSkipped; } diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index afb0ef135d..6bb62ca715 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -110,7 +110,7 @@ - On a device without VK_EXT_robustness2's `nullDescriptor`, such as MoltenVK, a null image descriptor in a typed heap is a [placeholder image](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp) of that heap's type, 1×1 and cleared to zero, which every null descriptor of the heap shares. Reads give zeros, as `nullDescriptor` allows, but stores and atomics to it are kept, so a later read of a null storage image can return them, and a depth comparison compares against depth 0. A heap whose placeholder the device cannot create, for example 1D or 3D depth, 64-bit atomics without 64-bit image atomics, or multisampled storage without `shaderStorageImageMultisample`, throws when a null descriptor reaches it. - [Draw input snapshots](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`. - [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver), except drawn resident linear targets (next entry), are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. Explicit-size file writes (`write`, `pwrite`, `writev`, `pwritev`, AIO and `fwrite`) synchronize their source ranges through the [host-read hook](../../../core/libs/prx/libc/src/GuestArena.cpp); other host readers are not covered. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default. -- [Resident linear color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) written by recorded draws are stored before the graphics queue's next flip, label, end-of-pipe interrupt, drain or device wait instead of when the draws complete, and each store copies only the union of the draws' scissor rectangles since the last store. A CPU store made before that point inside the rectangle, on a pixel no primitive wrote, is overwritten by the store, where the console keeps it; CPU stores outside the rectangle stay, whether or not the write watch saw them. A target with an attachment proxy, several mips or layers, a 3D or DCC-compressed surface, or results of a dispatch, fill or copy is stored whole and keeps main's rule that CPU-written 64 KiB blocks stay the CPU's, so GPU results in such a block are dropped. A store the draw pipeline's committer makes (for a label, an end-of-pipe interrupt or a device wait it commits) skips a target whose range a queued draw still reads (`DrawPipeline::HeldReads`). +- [Resident linear color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver): a CPU store inside a drawn scissor rectangle, on a pixel no primitive wrote, made before the target's store is overwritten by it, where the console keeps it. Recorded draws' results are stored before the graphics queue's next flip, label, end-of-pipe interrupt, drain or device wait, as the union of their scissor rectangles; a draw that brings a 17th rectangle that does not merge stores the target at once. A target with an attachment proxy, several mips or layers, a 3D or DCC surface, or results of a dispatch, fill or copy is stored after each draw. One that a fill or copy writes after its draws is stored whole at the next of those points, and GPU results in its CPU-written 64 KiB blocks are dropped. A store the draw pipeline's committer makes (label, end-of-pipe interrupt, device wait) skips a target whose range a queued draw still reads (`DrawPipeline::HeldReads`). - [`APS5_SKIP_REFUSED_DRAWS`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp) (libSceAgcDriver) drops a draw whose graphics stage the recompiler refuses instead of failing the queue, so the frame lacks that draw, and the stage is compiled and refused again at each such draw. Compute refusals still fail. - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending saves its bytes through the unwatch hook, which also runs when a failed collect takes a range out of the watch. A range that leaves the watch without the hook running over it (an import fallback that unwatched without saving) leaves its pending results without saved bytes; storing such results throws. A unit whose results were stored while its blocks were watched keeps only a generation; when its blocks leave the watch while nothing is pending, the hook saves nothing, and results that land in it afterwards have neither saved bytes nor a watch, so storing them throws too. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory when the import probe keeps the write watch and a settle queue exists: those of 1 MiB or more stay watched, and smaller ones return to the watch once their settle has completed, unless a storage image overlaps them (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. From ff4b2836d553b6ec8471a5457adbe3282b57d8d8 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Thu, 22 Oct 2026 21:25:55 +0700 Subject: [PATCH 118/428] fix(agc): prepare a draw's snapshot bindings after its drawn-target store The store of the drawn linear targets before a recorded draw ran after PrepareDrawBindings, which keeps the draw's snapshot descriptor sets in the batch open at that moment only. When the store synced that batch (an earlier address-based draw in it reads the target's memory) or submitted it, the draw was bound in the next batch with nothing keeping its sets. They were recycled once the draw was recorded, and the next draw with the same set layout rewrote them before the first draw ran, which then read that draw's buffers. The bindings are now prepared after the store, in the batch the draw is recorded in, and their snapshots skip memory the store writes. LinearTargetDrawBindings renders a linear target with an address-based draw, then runs two draws that each copy a snapshotted buffer into a copied output. The first one stores the target, which syncs the batch. Both outputs, both targets and the descriptor sets the two copying draws allocate are checked. Co-Authored-By: Claude --- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 2 +- .../execution/LinearTargetDrawBindings.cpp | 329 ++++++++++++++++++ 2 files changed, 330 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDrawBindings.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 09fa6e7079..e8fedd1d25 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1955,7 +1955,6 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam recorder->FlushStores(); outcome.storesFlushed = true; } - const auto drawBindings = resources.PrepareDrawBindings(*recorder, record.moved); const bool capture = CaptureInputsEnabled(); const bool meshIndirect = state.stages.mesh && args != nullptr; const bool addressBased = resources.HoldsLease(); @@ -1978,6 +1977,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam for (const auto image : record.multisampleImages) passAttachments.emplace_back(image, true); const bool unknownMemory = addressBased || resources.UsesAddressTable() || resources.HasCopiedWrites(); static_cast(StorageTexture::StoreDrawnLinearTouching("draw access", [&](std::uint64_t begin, std::uint64_t end) { return std::any_of(passReads.begin(), passReads.end(), [&](const auto& read) { return read.first < end && begin < read.second; }) || resources.WritesOverlap(begin, static_cast(end - begin)); }, unknownMemory)); + const auto drawBindings = resources.PrepareDrawBindings(*recorder, record.moved); static const bool addressReadPasses = std::getenv("APS5_NO_ADDRESS_READ_PASSES") == nullptr; const bool readOnlyBda = addressReadPasses && resources.BdaReadOnly(); outcome.addressDraw = addressBased; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDrawBindings.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDrawBindings.cpp new file mode 100644 index 0000000000..eee2098849 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDrawBindings.cpp @@ -0,0 +1,329 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t TargetBytes = std::size_t{Width} * Height * 16u; +constexpr std::size_t CopyTargetOffset = 0x40000; +constexpr std::size_t FirstSourceOffset = 0x80000; +constexpr std::size_t SecondSourceOffset = 0x90000; +constexpr std::size_t PointedOffset = 0xa0000; +constexpr std::uint32_t Rgba32FloatInfo = 0x8738u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32AbgrExport = 9; +constexpr std::uint32_t FirstSource = 0x3e800000u; +constexpr std::uint32_t SecondSource = 0x3f400000u; +constexpr std::uint32_t Pointed = 0x3f800000u; +constexpr std::uint32_t Unwritten = 0xdeadbeefu; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelOpaque{0x7e100280u, 0x7e120280u, 0x7e140280u, 0x7e1602f2u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; +alignas(256) constexpr std::array PixelRead{0x7e1a0280u, 0xdc308000u, 0x0c02000du, 0xbf8c3f70u, 0x7e10030cu, 0x7e120280u, 0x7e140280u, 0x7e160280u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; +alignas(256) constexpr std::array PixelCopy{0x7e040280u, 0xe0301000u, 0x80010102u, 0xbf8c3f70u, 0xe0701000u, 0x80020102u, 0x7e100301u, 0x7e120280u, 0x7e140280u, 0x7e1602f2u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array FirstOutput{}; +alignas(256) std::array SecondOutput{}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +std::array RawBuffer(std::uint64_t address, std::uint32_t bytes) { + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color targets"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color targets"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color targets are not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(address >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Rgba32FloatInfo; + cx[0x31d] = 0; + cx[0x323] = 0; + cx[0x324] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = LinearAttrib3; + cx[0x390] = static_cast(address >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + color.exportIndex = 0; + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::span pixelCode, std::span pixelUserData, VkRect2D scissor) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionFixedPoint); + pixel.targetOutputMode.at(0) = Float32AbgrExport; + pixel.targetExportMapping.fill(0xe4u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + VkPipelineColorBlendAttachmentState blend{}; + blend.blendEnable = VK_FALSE; + blend.colorWriteMask = 0xf; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = scissor; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::array CopyUserData(std::uint64_t source, const std::array& output) { + std::array userData{}; + const auto read = RawBuffer(source, 16u); + const auto written = RawBuffer(reinterpret_cast(output.data()), 16u); + std::copy(read.begin(), read.end(), userData.begin() + 4); + std::copy(written.begin(), written.end(), userData.begin() + 8); + return userData; +} + +std::array Texel(const std::uint8_t* target, std::uint32_t x, std::uint32_t y) { + std::array texel{}; + std::memcpy(texel.data(), target + (static_cast(y) * Width + x) * 16u, sizeof(texel)); + return texel; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::string Describe(const std::array& texel) { + return "(" + std::to_string(texel[0]) + ", " + std::to_string(texel[1]) + ", " + std::to_string(texel[2]) + ", " + std::to_string(texel[3]) + ")"; +} + +void ExpectTexels(const std::uint8_t* target, const char* what, std::uint32_t firstX, std::uint32_t lastX, const std::array& expected) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = firstX; x < lastX; ++x) { + const auto texel = Texel(target, x, y); + Require(texel == expected, std::string(what) + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds " + Describe(texel) + ", expected " + Describe(expected)); + } + } +} + +bool Expect(AgcDriver::VulkanDevice& device) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if (!context.bufferDeviceAddress) { + std::puts("skipped, the device lacks bufferDeviceAddress"); + return false; + } + Block block(16 * BlockBytes); + std::fill(block.data, block.data + block.bytes, std::uint8_t{0}); + const auto put = [&](std::size_t offset, std::uint32_t value, std::size_t dwords) { + for (std::size_t dword = 0; dword < dwords; ++dword) std::memcpy(block.data + offset + dword * 4u, &value, sizeof(value)); + }; + put(FirstSourceOffset, FirstSource, 4); + put(SecondSourceOffset, SecondSource, 4); + put(PointedOffset, Pointed, 1); + FirstOutput.fill(Unwritten); + SecondOutput.fill(Unwritten); + if (AgcDriver::Graphics::HostImportFor(context, block.Address(), block.bytes) == nullptr) { + std::puts("skipped, host imports are unavailable"); + return false; + } + const auto drawn = DecodeTarget(block.Address()); + const auto copied = DecodeTarget(block.Address() + CopyTargetOffset); + Require(drawn.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && drawn.bytes == TargetBytes && copied.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && copied.bytes == TargetBytes, "the targets are not linear R32G32B32A32 targets"); + auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr && context.descriptorCache != nullptr, "the device has no recorder or descriptor cache"); + const auto pending = [](std::uint64_t address) { return StorageTexture::FindPending(address, TargetBytes) != nullptr; }; + const VkRect2D whole{{0, 0}, {Width, Height}}; + Draw(device, drawn, PixelOpaque, {}, whole); + Draw(device, copied, PixelOpaque, {}, whole); + device.WaitIdle(); + Require(!pending(drawn.address) && !pending(copied.address), "a linear target was still pending after the device wait"); + const auto submitted = recorder->Submissions(); + const auto pointed = block.Address() + PointedOffset; + const std::array pointer{0u, 0u, static_cast(pointed), static_cast(pointed >> 32u)}; + Draw(device, drawn, PixelRead, pointer, whole); + Require(pending(drawn.address), "the address-based draw did not leave its linear target pending"); + Require(recorder->Recording() && recorder->Submissions() == submitted, "the address-based draw did not stay in the open batch"); + const auto setsBefore = context.descriptorCache->Counters().sets; + const auto firstCopy = CopyUserData(block.Address() + FirstSourceOffset, FirstOutput); + Draw(device, copied, PixelCopy, firstCopy, {{0, 0}, {Width / 2u, Height}}); + Require(!pending(drawn.address), "the draw writing a copied buffer did not store the drawn linear target first"); + Require(pending(copied.address), "the first copying draw did not leave its linear target pending"); + Require(recorder->Recording() && recorder->Submissions() == submitted + 1, "storing the drawn linear target did not submit the batch the address-based draw read it in (" + std::to_string(recorder->Submissions() - submitted) + " submissions)"); + const auto setsBetween = context.descriptorCache->Counters().sets; + const auto secondCopy = CopyUserData(block.Address() + SecondSourceOffset, SecondOutput); + Draw(device, copied, PixelCopy, secondCopy, {{static_cast(Width / 2u), 0}, {Width / 2u, Height}}); + Require(recorder->Recording() && recorder->Submissions() == submitted + 1, "the second copying draw submitted recorded work"); + const auto setsAfter = context.descriptorCache->Counters().sets; + device.WaitIdle(); + Require(FirstOutput[0] == FirstSource, "the first copying draw's output holds " + Hex(FirstOutput[0]) + ", not its source's " + Hex(FirstSource)); + Require(SecondOutput[0] == SecondSource, "the second copying draw's output holds " + Hex(SecondOutput[0]) + ", not its source's " + Hex(SecondSource)); + ExpectTexels(block.data + CopyTargetOffset, "the first copying draw's half", 0, Width / 2u, {0.25f, 0.0f, 0.0f, 1.0f}); + ExpectTexels(block.data + CopyTargetOffset, "the second copying draw's half", Width / 2u, Width, {0.75f, 0.0f, 0.0f, 1.0f}); + ExpectTexels(block.data, "the address-based draw's target", 0, Width, {1.0f, 0.0f, 0.0f, 0.0f}); + Require(setsAfter - setsBetween == setsBetween - setsBefore, "the second copying draw allocated " + std::to_string(setsAfter - setsBetween) + " descriptor sets, the first " + std::to_string(setsBetween - setsBefore)); + return true; +} + +} + +int main() { +#ifdef _WIN32 + _putenv_s("APS5_RELEASE_ON_UNLOCK", "1"); +#else + setenv("APS5_RELEASE_ON_UNLOCK", "1", 1); +#endif + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!Expect(*device)) return VulkanTestSkipped; + std::puts("linear target draw binding tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 9427af76c0d19d6a2f1791d9ef36a140d8ef3129 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Thu, 22 Oct 2026 21:46:20 +0700 Subject: [PATCH 119/428] test(agc): cover the drawn linear target store before an address-based dispatch A draw into a resident linear target only lists it, and an address-based build stores no pending image over the leased heaps it maps, so before such a dispatch the target reaches memory only through the store recordDispatch makes of the drawn targets the dispatch touches. LinearTargetDispatchAccess draws into a resident linear target under a scissor, then, with no completion point in between, runs an address-based dispatch that loads a drawn texel through a pointer and stores it into another registered block, which must hold the drawn value. Without that store the dispatch reads the bytes from before the draw; with main's store after each draw it reads the drawn value too. Co-Authored-By: Claude --- .../execution/LinearTargetDispatchAccess.cpp | 300 ++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDispatchAccess.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDispatchAccess.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDispatchAccess.cpp new file mode 100644 index 0000000000..809ffd4d91 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDispatchAccess.cpp @@ -0,0 +1,300 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t Threads = 64; +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t TargetBytes = std::size_t{Width} * Height * 16u; +constexpr std::uint32_t Rgba32FloatInfo = 0x8738u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32AbgrExport = 9; +constexpr std::uint32_t ReadX = 10; +constexpr std::uint32_t ReadY = 6; +constexpr std::uint32_t Drawn = 0x3f800000u; +constexpr std::uint32_t Unwritten = 0xdeadbeefu; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array CopyCode{0x7e000280u, 0xdc308000u, 0x01000000u, 0xbf8c3f70u, 0xdc708000u, 0x00020100u, 0xbf810000u}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 2> Programs{}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +std::span Program(std::size_t index, bool blue) { + auto& code = Programs.at(index); + std::size_t at = 0; + code[at++] = 0x36000000u | (20u << 17u) | 0xffu; + code[at++] = 0x0000ffffu; + code[at++] = 0x2c000000u | (21u << 17u) | 0x90u; + code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 20u); + code[at++] = 0x7e000c00u | (21u << 17u) | (256u + 21u); + code[at++] = (blue ? 0x7e0002f2u : 0x7e000280u) | (22u << 17u); + code[at++] = 0x7e0002f2u | (23u << 17u); + code[at++] = 0xf800180fu; + code[at++] = 0x17161514u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map a guest block"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate a guest block"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "a guest block is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Rgba32FloatInfo; + cx[0x31d] = 0; + cx[0x323] = 0; + cx[0x324] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = LinearAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + color.exportIndex = 0; + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::span pixelCode, VkRect2D scissor) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionFixedPoint); + pixel.targetOutputMode.at(0) = Float32AbgrExport; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + VkPipelineColorBlendAttachmentState blend{}; + blend.blendEnable = VK_FALSE; + blend.colorWriteMask = 0xf; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = scissor; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +void Dispatch(AgcDriver::VulkanDevice& device, std::uint64_t source, std::uint64_t destination) { + constexpr std::uint32_t waveSize = 64; + const std::array userData{static_cast(source), static_cast(source >> 32u), static_cast(destination), static_cast(destination >> 32u)}; + const std::span code(CopyCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t Dword(const std::uint8_t* bytes) { + std::uint32_t value = 0; + std::memcpy(&value, bytes, sizeof(value)); + return value; +} + +bool Expect(AgcDriver::VulkanDevice& device) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if (!context.bufferDeviceAddress) { + std::puts("skipped, the device lacks bufferDeviceAddress"); + return false; + } + Block block(16 * BlockBytes); + std::fill(block.data, block.data + block.bytes, std::uint8_t{0}); + Block output(BlockBytes); + for (std::size_t offset = 0; offset < output.bytes; offset += sizeof(Unwritten)) std::memcpy(output.data + offset, &Unwritten, sizeof(Unwritten)); + if (AgcDriver::Graphics::HostImportFor(context, block.Address(), block.bytes) == nullptr) { + std::puts("skipped, host imports are unavailable"); + return false; + } + const auto color = DecodeTarget(block); + Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && color.bytes == TargetBytes, "the target is not a linear R32G32B32A32 target"); + const auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr, "the device has no recorder"); + Draw(device, color, Program(0, false), {{0, 0}, {Width, Height}}); + device.WaitIdle(); + Require(StorageTexture::FindPending(block.Address(), TargetBytes) == nullptr, "the linear target was still pending after the device wait"); + const auto blueOffset = (static_cast(ReadY) * Width + ReadX) * 16u + 8u; + Require(Dword(block.data + blueOffset) == 0u, "the first draw left " + Hex(Dword(block.data + blueOffset)) + " in the blue channel of pixel (" + std::to_string(ReadX) + ", " + std::to_string(ReadY) + ")"); + Draw(device, color, Program(1, true), {{8, 4}, {16, 8}}); + Require(recorder->Recording(), "the draw into the linear target was not left in the open batch"); + Dispatch(device, block.Address() + blueOffset, output.Address()); + device.WaitIdle(); + const auto read = Dword(output.data); + Require(read == Drawn, "the address-based dispatch recorded after the draw read " + Hex(read) + " from the blue channel of pixel (" + std::to_string(ReadX) + ", " + std::to_string(ReadY) + "), not the drawn " + Hex(Drawn)); + Require(Dword(block.data + blueOffset) == Drawn, "the second draw left " + Hex(Dword(block.data + blueOffset)) + " in the blue channel of pixel (" + std::to_string(ReadX) + ", " + std::to_string(ReadY) + ")"); + return true; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!Expect(*device)) return VulkanTestSkipped; + std::puts("linear target dispatch access tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 04d6bae776ce6d7d42e2cc78a5056917bc00f881 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Thu, 22 Oct 2026 21:46:49 +0700 Subject: [PATCH 120/428] fix(agc): store drawn linear targets before a synchronous draw that touches them Only a lean recorded draw stored the drawn linear targets whose memory it touches before it. A draw on the other path (synchronous because a color target is not resident, a stencil-replay build checks faults or APS5_SYNC_COMPLETION_DRAWS=1 is set, or recorded with APS5_DRAW_TRANSITIONS=1) found such a target still only listed. Its resource build stores the pending images over the memory it binds, but not over the leased heaps an address-based build maps, so a read through a pointer got the bytes from before the draw that rendered the target. The same store now runs on that path before the draw's command batch is made. A synchronous draw's batch submits the recorded work first, so the store lands before it in queue order. AddressPasses --synchronous-target-read renders a linear target, then, in the same submission, reads it through the address table in a draw into an unregistered target, which is not resident, so that draw is synchronous. It must read the drawn color, as it does on main. On a device where the linear target is not resident the rendering draw is synchronous too, and the case says it is skipped. The new agc_driver_address_passes_draw_transitions entry runs the --targets cases with APS5_DRAW_TRANSITIONS=1, so a recorded draw takes the other path; its linear case reads the bytes from before the rendering draw without this store. Co-Authored-By: Claude --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 +++++ .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 15 ++++++----- .../tests/execution/AddressPasses.cpp | 27 +++++++++++++++++++ 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index ff1b5232a6..b221b4c2e5 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2052,6 +2052,12 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_address_passes_targets_split PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_address_passes_draw_transitions COMMAND agc_driver_address_passes_tests --targets) + set_tests_properties(agc_driver_address_passes_draw_transitions PROPERTIES ENVIRONMENT "APS5_NO_ADDRESS_READ_PASSES=1;APS5_DRAW_TRANSITIONS=1" SKIP_RETURN_CODE 77 TIMEOUT 60) + if(WIN32) + set_tests_properties(agc_driver_address_passes_draw_transitions PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() foreach(agcExecutionTestName IN LISTS ANYPS5_AGC_EXECUTION_TESTS) if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/execution/${agcExecutionTestName}.cpp) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index e8fedd1d25..339157e2b0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2693,6 +2693,14 @@ void recordPreparedDraw(const Context& context, const State& state, const Pm4::D report(outcome.waited ? (outcome.reason == SyncLease ? " recorded then waited (lease)" : " recorded then waited (copied writes)") : completion ? " recorded with completion" : " recorded"); return; } + const bool gpuIndirect = args != nullptr && indirect.path == IndirectDrawPath::Gpu; + const auto touches = [&](std::uint64_t begin, std::uint64_t end) { + const auto bytes = static_cast(end - begin); + if (resources->WritesOverlap(begin, bytes) || resources->ReadsOverlap(begin, bytes)) return true; + if (!gpuIndirect) return false; + return (begin < args->arguments + args->RangeBytes() && args->arguments < end) || (args->countIndirect && begin < args->countAddress + 4 && args->countAddress < end); + }; + static_cast(StorageTexture::StoreDrawnLinearTouching("draw access", [&](std::uint64_t begin, std::uint64_t end) { return touches(begin, end) || std::any_of(inputs.inPlaceReads.begin(), inputs.inPlaceReads.end(), [&](const auto& read) { return read.first < end && begin < read.second; }); }, resources->HoldsLease() || resources->UsesAddressTable() || resources->HasCopiedWrites())); APS5_LOG_CHARS_OUT_DEBUG("Creating CommandBatch"); std::optional batch; if (!recorded) { @@ -2702,16 +2710,9 @@ void recordPreparedDraw(const Context& context, const State& state, const Pm4::D } using CommandClass = Recorder::CommandClass; const auto countBarrier = [&](std::uint32_t count = 1) { if (recorded) Recorder::CountBarriers(CommandClass::Draw, count); }; - const bool gpuIndirect = args != nullptr && indirect.path == IndirectDrawPath::Gpu; // A queued DCC key store over memory the draw writes or reads in place (unknown for an // address-based build), or over its GPU-side records, lands before it, as before a dispatch // (VulkanDevice::dispatch). - const auto touches = [&](std::uint64_t begin, std::uint64_t end) { - const auto bytes = static_cast(end - begin); - if (resources->WritesOverlap(begin, bytes) || resources->ReadsOverlap(begin, bytes)) return true; - if (!gpuIndirect) return false; - return (begin < args->arguments + args->RangeBytes() && args->arguments < end) || (args->countIndirect && begin < args->countAddress + 4 && args->countAddress < end); - }; if (recorded && recorder->HasQueuedKeyStores() && (resources->HoldsLease() || recorder->AnyQueuedKeyStore(touches))) recorder->FlushKeyStores(); // A queued label store over such memory likewise (Recorder::RecordStore). if (recorded && recorder->HasQueuedStores() && (resources->HoldsLease() || recorder->AnyQueuedStore(touches))) recorder->FlushStores(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AddressPasses.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AddressPasses.cpp index 9278d67787..bc4e3ffab8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AddressPasses.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AddressPasses.cpp @@ -141,6 +141,7 @@ struct Step { const std::uint32_t* pixel; std::uint64_t pointer; std::array descriptor{}; + std::uint64_t color = 0; }; void Submit(const GuestBlock& vertices, const GuestBlock& target, std::initializer_list steps, bool linear = false) { @@ -179,6 +180,10 @@ void Submit(const GuestBlock& vertices, const GuestBlock& target, std::initializ Append(commands, 0x76, {0x008, static_cast(pixel >> 8u), static_cast(pixel >> 40u)}); Append(commands, 0x76, {0x00e, static_cast(step.pointer), static_cast(step.pointer >> 32u)}); Append(commands, 0x76, {0x010, step.descriptor[0], step.descriptor[1], step.descriptor[2], step.descriptor[3]}); + if (step.color != 0) { + Append(commands, 0x69, {0x318, static_cast(step.color >> 8u)}); + Append(commands, 0x69, {0x390, static_cast(step.color >> 40u)}); + } Append(commands, 0x2d, {3, 2}); } Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; @@ -285,6 +290,26 @@ void TargetReadAsWhenSplit(const GuestBlock& vertices, const GuestBlock& target, if (!linear && !split) Expect(after.passesContinued - before.passesContinued == 1, what + ": the reading draw did not continue the rendering draw's pass, so the case does not test it: " + Counts(before, after)); } +void TargetReadSynchronously(const GuestBlock& vertices, const GuestBlock& target, const GuestBlock& unmapped) { + Submit(vertices, target, {{PixelRead.data(), target.At(ColorOffset)}}, true); + std::memset(target.Bytes(ColorOffset), 0, ColorBytes); + std::memset(unmapped.Bytes(ColorOffset), 0, ColorBytes); + const auto before = AddressDraws(); + const auto otherBefore = OtherDraws(); + Submit(vertices, target, {{PixelColorWord.data(), 0}, {PixelRead.data(), target.At(ColorOffset), {}, unmapped.At(ColorOffset)}}, true); + const auto after = AddressDraws(); + const auto otherAfter = OtherDraws(); + const unsigned red = unmapped.Bytes(ColorOffset)[0]; + std::printf("a synchronous draw reading through the table the linear target the previous draw rendered read %s (red byte %u); recorded: %s, %s\n", red == 255u ? "that draw's color" : "the bytes before it", red, Counts(before, after).c_str(), Counts(otherBefore, otherAfter, "other draws").c_str()); + if (otherAfter.draws == otherBefore.draws) { + std::puts("a synchronous draw reading through the table the linear target the previous draw rendered: skipped, that draw was not recorded because the linear target is not resident on this device, so the target was stored right after it"); + return; + } + Expect(otherAfter.draws - otherBefore.draws == 1, "the draw rendering the linear target was not recorded as one draw: " + Counts(otherBefore, otherAfter, "other draws")); + Expect(after.draws == before.draws, "the draw reading through the table into an unregistered target was recorded, so the case does not test a synchronous draw: " + Counts(before, after)); + Expect(red == 255u, "a synchronous draw reading through the table the linear target the previous draw rendered read the bytes before it (red byte " + std::to_string(red) + "), not that draw's color"); +} + } int main(int argc, char** argv) { @@ -306,6 +331,7 @@ int main(int argc, char** argv) { GuestBlock readOnlyVertices; GuestBlock data; GuestBlock target; + GuestBlock unmapped; FillVertices(readOnlyVertices); readOnlyVertices.Map(false); data.Map(true); @@ -320,6 +346,7 @@ int main(int argc, char** argv) { TargetReadAsWhenSplit(readOnlyVertices, target, false); TargetReadAsWhenSplit(readOnlyVertices, target, true); } + if (only.empty() || only == "--synchronous-target-read") TargetReadSynchronously(readOnlyVertices, target, unmapped); AgcDriverShutdown_nid_postfix(); std::puts("address passes: draws reading through the BDA table share a render pass, and a store through it still begins one"); return 0; From cee2512ef81290314d1aba4a4a1d49c700afaf07 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Fri, 23 Oct 2026 12:13:33 +0700 Subject: [PATCH 121/428] test(agc): expect a capture submit when a drawn target's memory is unwatched agc_driver_linear_target_draw_bindings required the second copying draw to stay in the open batch. That holds only while the copied target's memory is write-watched. The draw's own store of the drawn linear targets stores the copied target too, and when its memory is not watched (a host import that is compared, as on llvmpipe), marking the target drawn again captures its guest bytes, which syncs through the open batch: one more submission, after the draw is recorded. The test now runs its scenario twice: once as the device sets it up, and once with the block taken out of the write watch after the first draws. A watched target must not submit; an unwatched one must submit exactly once. The descriptor-set and output checks run in both. Co-Authored-By: Claude --- .../tests/execution/LinearTargetDrawBindings.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDrawBindings.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDrawBindings.cpp index eee2098849..4335e86e2c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDrawBindings.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDrawBindings.cpp @@ -250,7 +250,7 @@ void ExpectTexels(const std::uint8_t* target, const char* what, std::uint32_t fi } } -bool Expect(AgcDriver::VulkanDevice& device) { +bool Expect(AgcDriver::VulkanDevice& device, bool unwatch) { const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); if (!context.bufferDeviceAddress) { std::puts("skipped, the device lacks bufferDeviceAddress"); @@ -281,6 +281,9 @@ bool Expect(AgcDriver::VulkanDevice& device) { Draw(device, copied, PixelOpaque, {}, whole); device.WaitIdle(); Require(!pending(drawn.address) && !pending(copied.address), "a linear target was still pending after the device wait"); + if (unwatch) AgcDriver::GuestMemory::Unwatch(block.Address(), block.bytes); + const bool watched = AgcDriver::GuestMemory::Watched(copied.address, TargetBytes); + Require(!unwatch || !watched, "the copied target is still write-watched after its block was taken out of the write watch"); const auto submitted = recorder->Submissions(); const auto pointed = block.Address() + PointedOffset; const std::array pointer{0u, 0u, static_cast(pointed), static_cast(pointed >> 32u)}; @@ -296,7 +299,8 @@ bool Expect(AgcDriver::VulkanDevice& device) { const auto setsBetween = context.descriptorCache->Counters().sets; const auto secondCopy = CopyUserData(block.Address() + SecondSourceOffset, SecondOutput); Draw(device, copied, PixelCopy, secondCopy, {{static_cast(Width / 2u), 0}, {Width / 2u, Height}}); - Require(recorder->Recording() && recorder->Submissions() == submitted + 1, "the second copying draw submitted recorded work"); + if (watched) Require(recorder->Recording() && recorder->Submissions() == submitted + 1, "the second copying draw submitted recorded work"); + else Require(recorder->Submissions() == submitted + 2, "the second copying draw into a stored target outside the write watch did not submit once to capture its bytes (" + std::to_string(recorder->Submissions() - submitted - 1) + " submissions)"); const auto setsAfter = context.descriptorCache->Counters().sets; device.WaitIdle(); Require(FirstOutput[0] == FirstSource, "the first copying draw's output holds " + Hex(FirstOutput[0]) + ", not its source's " + Hex(FirstSource)); @@ -319,7 +323,8 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - if (!Expect(*device)) return VulkanTestSkipped; + if (!Expect(*device, false)) return VulkanTestSkipped; + if (!Expect(*device, true)) return VulkanTestSkipped; std::puts("linear target draw binding tests passed"); return 0; } catch (const std::exception& error) { From c442afa5d1d0f658322d451742729a2ffca9727b Mon Sep 17 00:00:00 2001 From: Evgeniy Kazarin Date: Fri, 23 Oct 2026 19:25:56 +0800 Subject: [PATCH 122/428] docs: name shared views as the pages the Windows write watch leaves read-only CONVENTIONS.md still listed stable resident runs of at least 256 KiB as the pages the write watch can leave read-only between collections on Windows, and pointed at the debt/libc.md entry this PR removes. Resident runs are no longer write-protected. What stays read-only between collections on Windows are the pages of shared views (shared memory, shared file mappings, and direct memory mapped at more than one guest address), which Collect arms; the Windows direct-memory backing entry in debt/libc.md describes the direct-memory case. Co-Authored-By: Claude Opus 5.5 --- docs/dev/CONVENTIONS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/CONVENTIONS.md b/docs/dev/CONVENTIONS.md index 87c674dac4..fdf0b83768 100644 --- a/docs/dev/CONVENTIONS.md +++ b/docs/dev/CONVENTIONS.md @@ -20,7 +20,7 @@ There's no need to add image files to the repository. You can add images to the Host code that writes into guest memory through the kernel or a native library must hold a `GuestArena::HostWrite` ([GuestArena.hpp](../../core/libs/prx/libc/include/GuestArena.hpp)) over the destination range for the whole write. This covers file reads (`ReadFile`, `read`, `pread`), socket receives, `DeviceIoControl` and other calls that fill a caller's buffer. -The write watch can leave guest pages read-only between collections: every watched page on macOS, and stable resident runs of at least 256 KiB on Windows (see the write-watch entries in [debt/libc.md](debt/libc.md)). A kernel-mode write into such a page doesn't fault into the watch. It fails instead, with `EFAULT` on macOS and `ERROR_NOACCESS` on Windows. `HostWrite` opens the pages and makes sure the write is recorded. +The write watch can leave guest pages read-only between collections: every watched page on macOS, and on Windows the pages of shared views (shared memory, shared file mappings, and direct memory mapped at more than one guest address), which it tracks by write protection (see the write-watch and Windows direct-memory backing entries in [debt/libc.md](debt/libc.md)). A kernel-mode write into such a page doesn't fault into the watch. It fails instead, with `EFAULT` on macOS and `ERROR_NOACCESS` on Windows. `HostWrite` opens the pages and makes sure the write is recorded. - A write that completes later on another thread (overlapped I/O, completion ports, asynchronous reads) is bracketed where the bytes land, not where the request was issued. - Ordinary CPU stores from host C++ code need no bracket: the write watch records them (through a fault where the page is protected). From 2375223a92690cfb798e4c9906dbc8eb5493e71b Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:00:47 +0200 Subject: [PATCH 123/428] fix(libSceNgs2): an all-zero sampler setup leaves an earlier setup untouched, as the PS5 module does The PS5 libSceNgs2.native module (firmware 3.20 and 4.03) accepts a setup with an all-zero format on a configured voice and keeps its format, so a play afterwards succeeds. The guard for a play on a voice with no channels would otherwise fail it. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceNgs2.native/src/Voice.cpp | 5 ++++- .../libSceNgs2.native/tests/Ngs2VoiceEvents.cpp | 16 ++++++++++++++++ docs/dev/debt/libSceNgs2.native.md | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index b1b95cd204..db5b629204 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -180,8 +180,11 @@ static void ApplyCommonParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param } static void SetupSampler(Ngs2Voice& voice, const Ngs2WaveformFormat& format) { + if (format.waveform_type == 0 && format.num_channels == 0 && format.sample_rate == 0) { + if (voice.channels == 0) voice.ResetSetup(); + return; + } voice.ResetSetup(); - if (format.waveform_type == 0 && format.num_channels == 0 && format.sample_rate == 0) return; if (format.waveform_type != SCE_NGS2_WAVEFORM_TYPE_PCM_I16L && format.waveform_type != SCE_NGS2_WAVEFORM_TYPE_ATRAC9) { throw std::runtime_error("NGS2: waveform type " + Ngs2Hex(format.waveform_type) + " is not implemented"); } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceEvents.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceEvents.cpp index 033ca97589..da4b91132b 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceEvents.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceEvents.cpp @@ -41,6 +41,21 @@ static void TestPlayWithoutSetup() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static void TestEmptyFormatKeepsTheSetup() { + const auto system = CreateSystem(); + const auto voice = Sampler(system); + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, SamplerSetup); + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{}); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + Require(Flags(voice) == SCE_NGS2_VOICE_STATE_FLAG_INUSE); + + const auto bare = Sampler(system); + Control(bare, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{}); + const auto play = PlayParam(); + Require(sceNgs2VoiceControl(bare, &play.header) == SCE_NGS2_ERROR_INVALID_VOICE_ACCESS); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + static void TestPlayAfterSetupInOneList() { const auto system = CreateSystem(); const auto voice = Sampler(system); @@ -81,6 +96,7 @@ static void TestNeverSetUpVoicesOfOtherRacks() { int main() { TestPlayWithoutSetup(); + TestEmptyFormatKeepsTheSetup(); TestPlayAfterSetupInOneList(); TestNeverSetUpVoicesOfOtherRacks(); return 0; diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 3ce25c933b..4b8c3f88f4 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -17,7 +17,7 @@ - [NGS2 mastering voice gain](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - the param id 0x30000004 is taken from PPSA22520 and the layout from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2MasteringVoiceGainParam`; no reference gives the units or range. The levels are assumed to be linear factors applied when the mastering voice is mixed into the render buffer, so the voice's own samples stay unscaled; the LFE level applies to channel 3 of 6- and 8-channel voices, assuming the L R C LFE order of the AudioOut2 entries above, and the full-band level to the rest. Negative levels are accepted, and a new setup resets both levels to 1 - [NGS2 stereo mastering voice into a surround buffer](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - PPSA21402 renders a 2-channel mastering voice into a 6- or 8-channel render buffer, which the mixing stage rejected. The console's conversion is unknown: the left and right channels are assumed to go to the first two buffer channels (front left and right) and the remaining channels stay silent, with no centre, LFE or surround content. Any other channel count mismatch still throws - [NGS2 voices without a format](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a sampler voice whose setup has no format (all zero) accepts an add of no waveform blocks without `APPEND`, which only applies `RESET` and the `CONTINUE` state, and matrix levels with no levels, which clear that matrix. PPSA36591 sets such a voice up, adds no blocks and sets no levels when a button is pressed at its title screen. Whether the console returns success or an error there is not measured; blocks with data, `APPEND` and an empty matrix on a voice with a format are still refused -- [NGS2 play without setup](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - differs from PS5 firmware 3.20 and 4.03 `libSceNgs2.native.sprx` in three cases: a set-up voice given an add of no waveform blocks and played keeps state flags 1 there for as long as it was watched and only until the next render here; an add of no blocks to a voice that already played and ended returns `0x804A8054` there and succeeds here; a playing voice reset by an all-zero setup keeps its state there and is stopped here. A `PLAY` on a voice with no channels returns `0x804A8351` (`SCE_NGS2_ERROR_INVALID_VOICE_ACCESS`) from Control and RunCommands, leaves it idle and skips the rest of the list, as that module does for every rack; PPSA03061 (Void Terrarium) frees its sound slots that way +- [NGS2 play without setup](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - differs from PS5 firmware 3.20 and 4.03 `libSceNgs2.native.sprx` in two cases: a set-up voice given an add of no waveform blocks and played keeps state flags 1 there for as long as it was watched and only until the next render here; an add of no blocks to a voice that already played and ended returns `0x804A8054` there and succeeds here. A `PLAY` on a voice with no channels (never set up; an all-zero setup leaves an earlier setup untouched) returns `0x804A8351` (`SCE_NGS2_ERROR_INVALID_VOICE_ACCESS`) from Control and RunCommands, leaves it idle and skips the rest of the list, as that module does for every rack; PPSA03061 (Void Terrarium) frees its sound slots that way - [NGS2 silent waveform blocks](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native): flag `0x10` (named `SILENCE` here; no public reference names it) lets a waveform block declare samples without data. PPSA04867 adds one block with flags `0x11`, `data_size` 0 and `num_samples` 16385, then another with flags `0x11` and a fully specified 0x800-byte, 1024-sample mono block, which takes the ordinary path. A dataless block under the flag outputs `num_samples` frames of silence, reports them through `decoded_data_size`, and finishes like a complete block; a dataless block without the flag still throws as not fitting its data. The flag's real name, its interaction with ATRAC9 and repeats, and the console's `waveform_data` for silent blocks are unverified on hardware - [NGS2 streaming PCM waveforms](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - PPSA21402 streams its music by adding one block whose `num_samples` is the whole track and whose `data_size` is a 16 KiB first piece, with `CONTINUE`, and then adds each following piece with flag `0x2` (called `APPEND` here) and the same `num_samples`. No reference describes flag `0x2`. A 16-bit PCM block without skipped samples or repeats whose `num_samples` exceed its data is assumed to start such a stream; flag `0x2` is assumed to append the block's data to the last streaming block, ignoring its `num_samples`; the voice is assumed to output silence without advancing while it has played all appended data and `CONTINUE` is set; and the waveform is assumed to end at `num_samples` or, once `CONTINUE` is cleared, at the end of the appended data. `sceNgs2VoiceGetState` reports the bytes played as `decoded_data_size`, which the title uses to release its queued data, and the read position as `waveform_data`. Appending to a block that is not a stream, or other formats, skipped samples, repeats and partial frames, throw - [NGS2 matrix levels](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - levels are clamped to -4..4 and NaN is kept, as the PS4 firmware 11.00 `libSceNgs2.sprx` stores them; not yet confirmed on the PS5 library. That library rejects a level count of 0 and keeps the matrix; the PS5 error code is unknown, so a count of 0 throws, except on a sampler voice without a format, where it clears the matrix (see NGS2 voices without a format). `sceNgs2VoiceRunCommands` is not in that library, so its matrix levels command is assumed to clamp the same way From e48a2f31033f9ef84bde0cb0b4df7ee22acdf7f3 Mon Sep 17 00:00:00 2001 From: hypermist Date: Sat, 24 Oct 2026 01:05:19 +1300 Subject: [PATCH 124/428] docs(libSceFont): cite firmware 13.60, not 12.00, for the decrypted modules The firmware version comes from the SDK 0x13600007 recorded in each module's module-param segment. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013XFS3FUfBtqCVfwmBrRQ6q --- docs/dev/debt/libSceFont.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index b62da4ea2b..54b60ddb79 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -24,7 +24,7 @@ - [Font graphics and metric signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): graphics-device/service creation takes font memory, parameters and a handle output; destruction takes a handle pointer. Attribute selectors and outputs are 32-bit values. Metric, outline-profile, style-information and expansion-buffer outputs remain opaque pending field recovery. Protected implementations are unchanged. -- [Font graphics fill descriptors](../../../core/libs/prx/libSceFont/src/Graphics.cpp): the region, fill-method, fill-plot and fill-rates initialisers and setters, and `sceFontSetGraphicsErrorCallback`, follow firmware 12.00 `libSceFont.sprx` (read from disassembly, not measured on a console): argument checks in the same order, error codes, tags and the bytes written. What the layout selectors, mapping values, region corner flags and fill effects mean is unknown, because the calls that read these descriptors (`sceFontGraphicsSetup*`, `sceFontGraphicsUpdate*`) still throw. The error codes `0x80460086` to `0x80460089` have no public names; the source names them after the argument they reject +- [Font graphics fill descriptors](../../../core/libs/prx/libSceFont/src/Graphics.cpp): the region, fill-method, fill-plot and fill-rates initialisers and setters, and `sceFontSetGraphicsErrorCallback`, follow firmware 13.60 `libSceFont.sprx` (read from disassembly, not measured on a console): argument checks in the same order, error codes, tags and the bytes written. What the layout selectors, mapping values, region corner flags and fill effects mean is unknown, because the calls that read these descriptors (`sceFontGraphicsSetup*`, `sceFontGraphicsUpdate*`) still throw. The error codes `0x80460086` to `0x80460089` have no public names; the source names them after the argument they reject Frame lifecycle, drawing completion, device usage, frame policy, clipping and fill setup/update argument signatures in [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered. Frame and resource layouts remain opaque; their runtime implementation is still unavailable. From 1153d177b648de8fcfc31e6988831543e362b5ca Mon Sep 17 00:00:00 2001 From: hypermist Date: Sat, 24 Oct 2026 01:05:38 +1300 Subject: [PATCH 125/428] docs(libSceSpSysCallWrapper): cite firmware 13.60, not 12.00, for the decrypted modules The firmware version comes from the SDK 0x13600007 recorded in each module's module-param segment. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013XFS3FUfBtqCVfwmBrRQ6q --- docs/dev/debt/libSceSpSysCallWrapper.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceSpSysCallWrapper.md b/docs/dev/debt/libSceSpSysCallWrapper.md index 74d9011159..b7e0a6e4d8 100644 --- a/docs/dev/debt/libSceSpSysCallWrapper.md +++ b/docs/dev/debt/libSceSpSysCallWrapper.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceSpGetPid, sceSpSelect and sceSpPthreadMutexattrSetprioceiling](../../../core/libs/prx/libSceSpSysCallWrapper/Export.cpp) (libSceSpSysCallWrapper) - still throw. `sceSpGetPid` and `sceSpSelect` are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSpSysCallWrapper.c) but not exported by firmware 12.00's `libSceSpSysCallWrapper.sprx`, so no prototype could be read. There, `sceSpPthreadMutexattrSetprioceiling` only jumps to libkernel's `scePthreadMutexattrSetprioceiling`, which AnyPS5 does not implement yet (read from disassembly, not measured on a console) +- [sceSpGetPid, sceSpSelect and sceSpPthreadMutexattrSetprioceiling](../../../core/libs/prx/libSceSpSysCallWrapper/Export.cpp) (libSceSpSysCallWrapper) - still throw. `sceSpGetPid` and `sceSpSelect` are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSpSysCallWrapper.c) but not exported by firmware 13.60's `libSceSpSysCallWrapper.sprx`, so no prototype could be read. There, `sceSpPthreadMutexattrSetprioceiling` only jumps to libkernel's `scePthreadMutexattrSetprioceiling`, which AnyPS5 does not implement yet (read from disassembly, not measured on a console) From 916312403c8fc8b65e06afebc65ff64d4d82f7d3 Mon Sep 17 00:00:00 2001 From: hypermist Date: Sat, 24 Oct 2026 01:06:01 +1300 Subject: [PATCH 126/428] docs(libScePsml): cite firmware 13.60, not 12.00, for the decrypted modules The firmware version comes from the SDK 0x13600007 recorded in each module's module-param segment. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013XFS3FUfBtqCVfwmBrRQ6q --- docs/dev/debt/libScePsml_debug.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libScePsml_debug.md b/docs/dev/debt/libScePsml_debug.md index b056b06354..b8bdedf383 100644 --- a/docs/dev/debt/libScePsml_debug.md +++ b/docs/dev/debt/libScePsml_debug.md @@ -3,7 +3,7 @@ ### Unknown function info - [scePsmlMfsrGetSharedResourcesInitRequirement, scePsmlMfsrCreateSharedResources, scePsmlMfsrReleaseSharedResources, scePsmlMfsrGetContextBufferRequirement800M3_2, scePsmlMfsrCreateContext800M3_2, scePsmlMfsrReleaseContext](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - only resource type 0 is supported. The context requirement reports four 2 MiB blocks regardless of its parameters, while the system calculation depends on the context configuration. The descriptor pointer at 0x28 is not read. Validation throws for null pointers, short block lists and foreign handles instead of reproducing the system error and fault behaviour. -- [scePsmlMfsr2Init and the other MFSR2 calls](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - only a console without PSSR is modelled. In firmware 12.00 `libScePsml.sprx` (read from disassembly, not measured on a console), `scePsmlMfsr2Init` returns 0x8A8A0012 when `sceKernelIsTrinityMode` returns 0, and until `Init` succeeds every other MFSR2 call returns 0x8A8A0001 without touching its arguments. AnyPS5 reports 0, so `Init` throws only if that changes: the initialization after the check, the shared resource and context requirements, and the 0x800-dword size that `GetDispatchPacketsSizeInDwords` writes through its second argument are not implemented +- [scePsmlMfsr2Init and the other MFSR2 calls](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - only a console without PSSR is modelled. In firmware 13.60 `libScePsml.sprx` (read from disassembly, not measured on a console), `scePsmlMfsr2Init` returns 0x8A8A0012 when `sceKernelIsTrinityMode` returns 0, and until `Init` succeeds every other MFSR2 call returns 0x8A8A0001 without touching its arguments. AnyPS5 reports 0, so `Init` throws only if that changes: the initialization after the check, the shared resource and context requirements, and the 0x800-dword size that `GetDispatchPacketsSizeInDwords` writes through its second argument are not implemented - [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; PPSA26344 imports but does not call the first two and at `0x87EFA` passes `(rdi, rsi=&stack, rdx)` as three pointers to `GetDispatchMfsrPacket1100`; before `scePsmlMfsrInit` they return 0x8A810001 (not initialized) and after it they throw. `scePsmlMfsrCreateContext1300` and `scePsmlMfsrGetDispatchMfsrPacket900`, `1000` and `1300` take the same shapes (context slot and parameter; context, command buffer and parameter) and behave the same way. PPSA08330 and PPSA03016 pass `GetDispatchMfsrPacket900`/`1000` the context handle, a `{cursor, size in dwords}` command buffer and the dispatch parameters, and advance their cursor by the size `scePsmlMfsrGetDispatchMfsrPacketSizeInDwords` returned. Whether the 1100 and 1300 versions take these arguments, and the console's error, are unverified - [scePsmlMfsrGetDispatchMfsrPacket1000](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - imported by PPSA25344. libScePsml and libScePsml_debug are built from the same source - [scePsmlMfsrCreateContext1300, scePsmlMfsrGetDispatchMfsrPacket1300](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - imported by PPSA28416 next to `scePsmlMfsrGetContextBufferRequirement1100`. The names are not in the aerolib NID list; they hash to the imported NIDs `kLF7WtQ1-Dg` and `DDVUyPyYHTA` (`tools/nid_names.py`) @@ -11,4 +11,4 @@ ### Functional - [scePsmlMfsrGetDispatchMfsrPacketSizeInDwords, scePsmlMfsrGetMipmapBias, scePsmlMfsrSelectConfig, scePsmlMfsrRequestCapture, scePsmlMfsrIsCaptureInProgress](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - semantics from the APR-EMU `fakelib/libScePsml.sprx`. Guards: `0x8A810001` uninitialized, `0x8A810005` null/foreign handle (fakelib checks an engine id `0xA9C4`/`0x9231`; our map check is the equivalent), `0x8A810009` null output. `GetDispatchMfsrPacketSizeInDwords` writes `0x600`; `GetMipmapBias(context, bias, dimension)` writes `log2f(dimension / context->inputWidth) - 1.0f`, width recorded at `CreateContext800M3_2` from the parameter pair at 0x18 or `descriptor0 + 0x20` for callers that pass a descriptor pointer there (indirection inferred; zero width returns `0x8A810005`). `SelectConfig`, `RequestCapture`, `IsCaptureInProgress` validate then return 0. The `GetDispatchMfsrPacket*` calls still throw: the 0x158-byte template's packet routine is not understood -- [scePsmlMfsrInit](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - succeeds and only sets the initialized state; the shared resources and contexts are bookkeeping objects that use none of the guest blocks. The console's library fails there instead: in firmware 12.00 (read from disassembly, not measured on a console) `scePsmlMfsrInit` returns 0x8A810012 when `sceKernelIsTrinityMode` returns 0, so its later MFSR calls return 0x8A810001. PPSA08330 calls the setup unconditionally and only uses a context when its cached `sceKernelIsTrinityMode` result is set (0x3c643d, 0x567f97; otherwise it stores a -0.25 mip bias itself at 0x567fe2), which AnyPS5 reports as 0 +- [scePsmlMfsrInit](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - succeeds and only sets the initialized state; the shared resources and contexts are bookkeeping objects that use none of the guest blocks. The console's library fails there instead: in firmware 13.60 (read from disassembly, not measured on a console) `scePsmlMfsrInit` returns 0x8A810012 when `sceKernelIsTrinityMode` returns 0, so its later MFSR calls return 0x8A810001. PPSA08330 calls the setup unconditionally and only uses a context when its cached `sceKernelIsTrinityMode` result is set (0x3c643d, 0x567f97; otherwise it stores a -0.25 mip bias itself at 0x567fe2), which AnyPS5 reports as 0 From 6ae116d7676581cc5cc59eeb7a4855c1121cb21b Mon Sep 17 00:00:00 2001 From: hypermist Date: Sat, 24 Oct 2026 01:08:47 +1300 Subject: [PATCH 127/428] docs(libSceSpSysCallWrapper): say firmware 13.60 exports sceSpGetPid and sceSpSelect The earlier entry said the firmware does not export them. It does: each is a single jump to libScePosix's getpid and select. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013XFS3FUfBtqCVfwmBrRQ6q --- docs/dev/debt/libSceSpSysCallWrapper.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceSpSysCallWrapper.md b/docs/dev/debt/libSceSpSysCallWrapper.md index b7e0a6e4d8..1d18e33722 100644 --- a/docs/dev/debt/libSceSpSysCallWrapper.md +++ b/docs/dev/debt/libSceSpSysCallWrapper.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceSpGetPid, sceSpSelect and sceSpPthreadMutexattrSetprioceiling](../../../core/libs/prx/libSceSpSysCallWrapper/Export.cpp) (libSceSpSysCallWrapper) - still throw. `sceSpGetPid` and `sceSpSelect` are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSpSysCallWrapper.c) but not exported by firmware 13.60's `libSceSpSysCallWrapper.sprx`, so no prototype could be read. There, `sceSpPthreadMutexattrSetprioceiling` only jumps to libkernel's `scePthreadMutexattrSetprioceiling`, which AnyPS5 does not implement yet (read from disassembly, not measured on a console) +- [sceSpGetPid, sceSpSelect and sceSpPthreadMutexattrSetprioceiling](../../../core/libs/prx/libSceSpSysCallWrapper/Export.cpp) (libSceSpSysCallWrapper) - still throw. In firmware 13.60's `libSceSpSysCallWrapper.sprx`, `sceSpGetPid` and `sceSpSelect` are single jumps to libScePosix's `getpid` and `select`, and `sceSpPthreadMutexattrSetprioceiling` only jumps to libkernel's `scePthreadMutexattrSetprioceiling`, which AnyPS5 does not implement yet (read from disassembly, not measured on a console) From e820df8c9dfd289379269ee32de8ea3fd10918d6 Mon Sep 17 00:00:00 2001 From: hypermist Date: Sat, 24 Oct 2026 01:15:24 +1300 Subject: [PATCH 128/428] feat(libSceSpSysCallWrapper): forward sceSpGetPid and sceSpSelect Both are exported by firmware 13.60 libSceSpSysCallWrapper.sprx as a single jmp to the getpid and select stubs. Forward them and trim the debt entry to sceSpPthreadMutexattrSetprioceiling. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceSpSysCallWrapper/Export.cpp | 10 ++++---- core/libs/tests/GuestSpSysCallWrapper.cpp | 23 +++++++++++++++++++ docs/dev/debt/libSceSpSysCallWrapper.md | 2 +- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceSpSysCallWrapper/Export.cpp b/core/libs/prx/libSceSpSysCallWrapper/Export.cpp index e5883ecdd1..961780c901 100644 --- a/core/libs/prx/libSceSpSysCallWrapper/Export.cpp +++ b/core/libs/prx/libSceSpSysCallWrapper/Export.cpp @@ -7,6 +7,7 @@ extern "C" { std::int64_t APS5_VABI GuestNetRecv_nid_no_patch(int s, void* buf, std::size_t len, int flags); std::int64_t APS5_VABI GuestNetSend_nid_no_patch(int s, const void* buf, std::size_t len, int flags); +int APS5_VABI getpid_nid_postfix(void); int APS5_VABI sceKernelGettimeofday(KernelTimeval* tp); int APS5_VABI sceKernelNanosleep(const KernelTimespec* rqtp, KernelTimespec* rmtp); int APS5_VABI sceNetConnect(int s, const void* addr, std::uint32_t addrlen); @@ -50,10 +51,10 @@ int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr* attr); int APS5_VABI scePthreadMutexattrSetprotocol(PthreadMutexattr* attr, int protocol); int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type); int APS5_VABI scePthreadSetprio(Pthread thread, int prio); +int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout); int APS5_VABI sceSpGetPid() { - NotImplemented_nid_no_patch(__func__); - return 0; + return getpid_nid_postfix(); } int APS5_VABI sceSpKernelGettimeofday(KernelTimeval* tp) { @@ -241,9 +242,8 @@ int APS5_VABI sceSpPthreadSetprio(Pthread thread, int prio) { return scePthreadSetprio(thread, prio); } -int APS5_VABI sceSpSelect() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSpSelect(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout) { + return select_nid_postfix(nfds, readfds, writefds, exceptfds, timeout); } } diff --git a/core/libs/tests/GuestSpSysCallWrapper.cpp b/core/libs/tests/GuestSpSysCallWrapper.cpp index 80bce86a0d..b29744d261 100644 --- a/core/libs/tests/GuestSpSysCallWrapper.cpp +++ b/core/libs/tests/GuestSpSysCallWrapper.cpp @@ -4,6 +4,8 @@ #include extern "C" { +int APS5_VABI sceSpGetPid(); +int APS5_VABI sceSpSelect(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout); int APS5_VABI sceSpKernelGettimeofday(KernelTimeval* tp); int APS5_VABI sceSpKernelNanosleep(const KernelTimespec* rqtp, KernelTimespec* rmtp); int APS5_VABI sceSpNetConnect(int s, const void* addr, std::uint32_t addrlen); @@ -66,6 +68,9 @@ int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedPa int APS5_VABI scePthreadMutexattrSetprotocol(PthreadMutexattr* attr, int protocol); int APS5_VABI scePthreadSetprio(Pthread thread, int prio); Pthread APS5_VABI scePthreadSelf(); +int APS5_VABI getpid_nid_postfix(void); +int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout); +int* APS5_VABI __error_nid_postfix(); } namespace { @@ -188,10 +193,28 @@ void Net() { SameError([&] { return sceSpNetResolverStartNtoa(-1, "localhost", &resolved, 0, 0, 0); }, [&] { return sceNetResolverStartNtoa(-1, "localhost", &resolved, 0, 0, 0); }); } +void Posix() { + Require(sceSpGetPid() > 0); + Require(sceSpGetPid() == getpid_nid_postfix()); + + const std::int64_t zero[2]{0, 0}; + Require(sceSpSelect(0, nullptr, nullptr, nullptr, zero) == 0); + const std::int64_t invalid[2]{0, 1000000}; + *__error_nid_postfix() = 0; + Require(sceSpSelect(0, nullptr, nullptr, nullptr, invalid) == -1); + const int wrappedErrno = *__error_nid_postfix(); + *__error_nid_postfix() = 0; + Require(select_nid_postfix(0, nullptr, nullptr, nullptr, invalid) == -1); + Require(wrappedErrno == 22 && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(sceSpSelect(-1, nullptr, nullptr, nullptr, nullptr) == -1 && *__error_nid_postfix() == 22); +} + } int main() { Threads(); Time(); Net(); + Posix(); } diff --git a/docs/dev/debt/libSceSpSysCallWrapper.md b/docs/dev/debt/libSceSpSysCallWrapper.md index 1d18e33722..1a664e18fd 100644 --- a/docs/dev/debt/libSceSpSysCallWrapper.md +++ b/docs/dev/debt/libSceSpSysCallWrapper.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceSpGetPid, sceSpSelect and sceSpPthreadMutexattrSetprioceiling](../../../core/libs/prx/libSceSpSysCallWrapper/Export.cpp) (libSceSpSysCallWrapper) - still throw. In firmware 13.60's `libSceSpSysCallWrapper.sprx`, `sceSpGetPid` and `sceSpSelect` are single jumps to libScePosix's `getpid` and `select`, and `sceSpPthreadMutexattrSetprioceiling` only jumps to libkernel's `scePthreadMutexattrSetprioceiling`, which AnyPS5 does not implement yet (read from disassembly, not measured on a console) +- [sceSpPthreadMutexattrSetprioceiling](../../../core/libs/prx/libSceSpSysCallWrapper/Export.cpp) (libSceSpSysCallWrapper) - still throws. In firmware 13.60's `libSceSpSysCallWrapper.sprx` it only jumps to libkernel's `scePthreadMutexattrSetprioceiling`, which AnyPS5 does not implement yet (read from disassembly, not measured on a console) From b6eb916a82c387de09f441b6a7d8567d8038b60b Mon Sep 17 00:00:00 2001 From: winstrex <79346005+winstrex@users.noreply.github.com> Date: Thu, 22 Oct 2026 03:08:50 -0300 Subject: [PATCH 129/428] perf(agc): present on a queue of its own outside the GPU mutex vkQueuePresentKHR blocks the presenter for a frame's pacing on some drivers (about 6 ms per present on NVIDIA in FIFO mode), and it ran under GuestMemory::GpuMutex because the main queue needs external synchronization with the recorder's submits. When the presentation family has a free queue, the device now takes it as a presentation queue guarded by its own mutex, and the presenter hands the image to it after releasing the GPU mutex. WaitIdle takes that mutex around vkDeviceWaitIdle. Without a free queue the present stays on the main queue under the GPU mutex as before. Co-Authored-By: Claude Opus 5.5 --- .../Execution/include/PresentMode.hpp | 7 ++++ .../Execution/include/VulkanDevice.hpp | 5 ++- .../Execution/src/Driver/Presentation.cpp | 23 ++++++++--- .../Execution/src/VulkanDevice.cpp | 31 +++++++++++--- core/libs/prx/libSceAgcDriver/tests/Flip.cpp | 5 +++ .../tests/execution/DeviceQueuePlan.cpp | 41 +++++++++++++++++-- 6 files changed, 97 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PresentMode.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PresentMode.hpp index cb448bbc44..17510d198b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PresentMode.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PresentMode.hpp @@ -3,6 +3,8 @@ #include #include +#include +#include #include #include #include @@ -22,6 +24,11 @@ inline VkPresentModeKHR SelectPresentMode(const char* requested, std::span SelectPresentQueue(std::uint32_t familyQueues, std::uint32_t queuesTaken) { + if (queuesTaken >= familyQueues) return std::nullopt; + return queuesTaken; +} + } #endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 2a5ed0ac82..45fa24b7ad 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -44,9 +44,11 @@ struct DeviceQueuePlan { bool sparseBinding = false; std::uint32_t sparseFamily = VK_QUEUE_FAMILY_IGNORED; std::uint32_t sparseIndex = 0; + bool present = false; + std::uint32_t presentIndex = 0; }; -DeviceQueuePlan PlanDeviceQueues(std::span families, std::uint32_t family, bool settles, bool sparseBinding); +DeviceQueuePlan PlanDeviceQueues(std::span families, std::uint32_t family, bool settles, bool sparseBinding, bool presents); class VulkanDevice { public: @@ -255,6 +257,7 @@ public: bool PresentDisplayBuffer(const DisplayBuffer& buffer); double FinishPresent(); void QueuePresent(); + bool PresentsOnOwnQueue() const; // At the flip packet (under the mutex): the recorder's submissions so far and, under // APS5_PROFILE_DRAW, how many in flight have not signaled (the frame record's batches_at_flip). void FlipBatches(std::uint64_t& submissions, std::uint64_t& unsignaled) const; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp index d071d049fc..651a96e5a3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp @@ -77,6 +77,7 @@ void Driver::Present(const PresentationWindow& window, const DisplayBuffer* buff bool submitted = false; bool presentable = false; bool trailing = false; + bool queueUnlocked = false; double waitedMs = 0; { GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Present); @@ -125,12 +126,20 @@ void Driver::Present(const PresentationWindow& window, const DisplayBuffer* buff } if (submitted && (syncFlip || inFlight != 0)) { - presenting->QueuePresent(); - timing.Mark("queue_present"); + if (presenting->PresentsOnOwnQueue()) { + queueUnlocked = true; + } else { + presenting->QueuePresent(); + timing.Mark("queue_present"); + } trailing = !syncFlip; submitted = false; } } + if (queueUnlocked) { + presenting->QueuePresent(); + timing.Mark("queue_present"); + } if (trailing) { waitedMs += presenting->FinishPresent(); timing.Mark("render_fence_wait"); @@ -139,9 +148,13 @@ void Driver::Present(const PresentationWindow& window, const DisplayBuffer* buff waitedMs = presenting->FinishPresent(); timing.Mark("render_fence_wait"); - GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Present); - std::lock_guard lock(GuestMemory::GpuMutex()); - presenting->QueuePresent(); + if (presenting->PresentsOnOwnQueue()) { + presenting->QueuePresent(); + } else { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Present); + std::lock_guard lock(GuestMemory::GpuMutex()); + presenting->QueuePresent(); + } timing.Mark("queue_present"); } gpuReady(context); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 80f77cc2b3..34750880f4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -82,7 +82,7 @@ namespace AgcDriver { -DeviceQueuePlan PlanDeviceQueues(std::span families, std::uint32_t family, bool settles, bool sparseBinding) { +DeviceQueuePlan PlanDeviceQueues(std::span families, std::uint32_t family, bool settles, bool sparseBinding, bool presents) { DeviceQueuePlan plan; const auto count = static_cast(families.size()); if (settles) { @@ -101,6 +101,13 @@ DeviceQueuePlan PlanDeviceQueues(std::span famili } } } + if (presents && family < count) { + if (const auto index = SelectPresentQueue(families[family].queueCount, plan.mainCount)) { + plan.present = true; + plan.presentIndex = *index; + plan.mainCount = *index + 1; + } + } if (!sparseBinding) return plan; const auto taken = [&](std::uint32_t index) { return plan.settle && index == plan.settleFamily ? plan.settleIndex + 1 : 0u; }; const auto binds = [&](std::uint32_t index) { return index != family && families[index].queueCount > taken(index) && (families[index].queueFlags & VK_QUEUE_SPARSE_BINDING_BIT) != 0; }; @@ -427,6 +434,8 @@ struct VulkanDevice::State { std::uint32_t settleFamily = 0; VkCommandPool settlePool = VK_NULL_HANDLE; std::mutex settleQueueMutex; + VkQueue presentQueue = VK_NULL_HANDLE; + std::mutex presentQueueMutex; bool memoryBudget = false; bool depthRangeUnrestricted = false; bool samplerAnisotropy = false; @@ -1654,7 +1663,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : queueInfo.queueFamilyIndex = family; queueInfo.queueCount = 1; queueInfo.pQueuePriorities = &priority; - const float priorities[2]{1.0f, 1.0f}; + const float priorities[3]{1.0f, 1.0f, 1.0f}; #ifdef _WIN32 const bool settles = true; #else @@ -1701,13 +1710,13 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : // Guest shaders routinely read past descriptor ranges; robust access turns that into zeros // instead of a GPU fault that loses the device. enabled.robustBufferAccess = available.robustBufferAccess; - const auto plan = PlanDeviceQueues(queues, family, settles, available.sparseBinding == VK_TRUE); + const auto plan = PlanDeviceQueues(queues, family, settles, available.sparseBinding == VK_TRUE, window != nullptr); const bool settleQueue = plan.settle; const auto settleFamily = plan.settleFamily; const auto settleIndex = plan.settleIndex; VkDeviceQueueCreateInfo queueInfos[3]{queueInfo, {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}, {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}}; queueInfos[0].queueCount = plan.mainCount; - queueInfos[0].pQueuePriorities = plan.mainCount == 2 ? priorities : &priority; + queueInfos[0].pQueuePriorities = plan.mainCount > 1 ? priorities : &priority; deviceInfo.queueCreateInfoCount = 1; const auto addQueue = [&](std::uint32_t queueFamily, std::uint32_t index) { for (std::uint32_t info = 1; info < deviceInfo.queueCreateInfoCount; ++info) { @@ -2050,6 +2059,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : } state->DeviceFunction("vkGetDeviceQueue")(state->device, family, 0, &state->queue); state->queueFamily = family; + if (plan.present) { + state->DeviceFunction("vkGetDeviceQueue")(state->device, family, plan.presentIndex, &state->presentQueue); + APS5_LOG_OUT("Presentation queue family=%u index=%u", family, plan.presentIndex); + } if (settleQueue) { state->settleFamily = settleFamily; state->DeviceFunction("vkGetDeviceQueue")(state->device, settleFamily, settleIndex, &state->settleQueue); @@ -2279,6 +2292,8 @@ void VulkanDevice::WaitIdle() { if (!state->recorder->Idle()) Graphics::Recorder::CountSync(0, __builtin_return_address(0)); state->recorder->Sync(); } + std::unique_lock presentQueueLock(state->presentQueueMutex, std::defer_lock); + if (state->presentQueue != VK_NULL_HANDLE) presentQueueLock.lock(); check(state->DeviceFunction("vkDeviceWaitIdle")(state->device), "vkDeviceWaitIdle"); APS5_LOG_CHARS_OUT_DEBUG("VulkanDevice::WaitIdle complete"); } @@ -3688,13 +3703,19 @@ void VulkanDevice::QueuePresent() { present.swapchainCount = 1; present.pSwapchains = &state->swapchain; present.pImageIndices = &index; - const auto presented = state->DeviceFunction("vkQueuePresentKHR")(state->queue, &present); + std::unique_lock presentQueueLock(state->presentQueueMutex, std::defer_lock); + if (state->presentQueue != VK_NULL_HANDLE) presentQueueLock.lock(); + const auto presented = state->DeviceFunction("vkQueuePresentKHR")(state->presentQueue != VK_NULL_HANDLE ? state->presentQueue : state->queue, &present); if (presented == VK_ERROR_OUT_OF_DATE_KHR || presented == VK_SUBOPTIMAL_KHR) state->extent = {0, 0}; else check(presented, "vkQueuePresentKHR"); timing.Mark("queue_present"); APS5_LOG_OUT_DEBUG("vkQueuePresentKHR queued imageIndex=%u", index); } +bool VulkanDevice::PresentsOnOwnQueue() const { + return state->presentQueue != VK_NULL_HANDLE; +} + ShaderRecompiler::SpirvTarget VulkanDevice::ComputeTarget(std::uint32_t waveSize) const { auto target = Target(); if (waveSize == 32u && state->computeWave32) target.subgroupSize = 32u; diff --git a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp index ec73dea01a..2ab823d703 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp @@ -417,6 +417,11 @@ void testPresentMode() { check(expectFailure([&] { SelectPresentMode("", all); }).find("must be fifo, fifo_latest_ready, mailbox or immediate") != std::string::npos, "an empty present mode name was accepted"); const std::array none{}; check(expectFailure([&] { SelectPresentMode(nullptr, none); }).find("does not support") != std::string::npos, "FIFO was selected on a surface without it"); + using AgcDriver::SelectPresentQueue; + check(SelectPresentQueue(16, 2) == 2u, "the presentation queue does not follow the main and settle queues"); + check(SelectPresentQueue(16, 1) == 1u, "the presentation queue does not follow the main queue alone"); + check(!SelectPresentQueue(2, 2).has_value(), "a presentation queue was taken from a family without a free queue"); + check(!SelectPresentQueue(1, 1).has_value(), "a presentation queue was taken from a family with one queue"); } int main(int argc, char** argv) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DeviceQueuePlan.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DeviceQueuePlan.cpp index ca3c05ab27..032a78ccc8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DeviceQueuePlan.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DeviceQueuePlan.cpp @@ -34,16 +34,40 @@ VkQueueFamilyProperties family(VkQueueFlags flags, std::uint32_t queues) { } std::string describe(const AgcDriver::DeviceQueuePlan& plan) { - return "main " + std::to_string(plan.mainCount) + " queues, settle " + (plan.settle ? std::to_string(plan.settleFamily) + ":" + std::to_string(plan.settleIndex) : std::string("none")) + ", sparse " + (!plan.sparseBinding ? std::string("none") : plan.sparseFamily == Own ? std::string("on the main queue") : std::to_string(plan.sparseFamily) + ":" + std::to_string(plan.sparseIndex)); + return "main " + std::to_string(plan.mainCount) + " queues, present " + (plan.present ? std::to_string(plan.presentIndex) : std::string("none")) + ", settle " + (plan.settle ? std::to_string(plan.settleFamily) + ":" + std::to_string(plan.settleIndex) : std::string("none")) + ", sparse " + (!plan.sparseBinding ? std::string("none") : plan.sparseFamily == Own ? std::string("on the main queue") : std::to_string(plan.sparseFamily) + ":" + std::to_string(plan.sparseIndex)); } void check(const Case& test) { - const auto plan = AgcDriver::PlanDeviceQueues(test.families, 0, test.settles, true); - const auto dense = AgcDriver::PlanDeviceQueues(test.families, 0, test.settles, false); + const auto plan = AgcDriver::PlanDeviceQueues(test.families, 0, test.settles, true, false); + const auto dense = AgcDriver::PlanDeviceQueues(test.families, 0, test.settles, false, false); Require(plan.mainCount == dense.mainCount && plan.settle == dense.settle && plan.settleFamily == dense.settleFamily && plan.settleIndex == dense.settleIndex, std::string("device queue plan, ") + test.name + ": sparse binding changed the queues: " + describe(dense) + " without it, " + describe(plan) + " with it"); Require(!dense.sparseBinding && dense.sparseFamily == Own, std::string("device queue plan, ") + test.name + ": a device without sparse binding got a sparse queue: " + describe(dense)); const bool expected = plan.mainCount == test.mainCount && plan.settle == test.settle && (!test.settle || (plan.settleFamily == test.settleFamily && plan.settleIndex == test.settleIndex)) && plan.sparseBinding == test.sparseBinding && plan.sparseFamily == test.sparseFamily && (test.sparseFamily == Own || plan.sparseIndex == test.sparseIndex); - Require(expected, std::string("device queue plan, ") + test.name + ": got " + describe(plan)); + Require(expected && !plan.present, std::string("device queue plan, ") + test.name + ": got " + describe(plan)); + const auto windowed = AgcDriver::PlanDeviceQueues(test.families, 0, test.settles, true, true); + const bool kept = windowed.settle == plan.settle && windowed.settleFamily == plan.settleFamily && windowed.settleIndex == plan.settleIndex && windowed.sparseBinding == plan.sparseBinding && windowed.sparseFamily == plan.sparseFamily && windowed.sparseIndex == plan.sparseIndex; + const bool counted = windowed.present ? windowed.presentIndex == plan.mainCount && windowed.mainCount == plan.mainCount + 1 : windowed.mainCount == plan.mainCount && plan.mainCount >= test.families[0].queueCount; + Require(kept && counted, std::string("device queue plan, ") + test.name + ": a window changed more than the presentation queue: " + describe(plan) + " without it, " + describe(windowed) + " with it"); +} + +struct WindowCase { + const char* name; + std::vector families; + bool settles; + std::uint32_t mainCount; + bool present; + std::uint32_t presentIndex; + bool settle; + std::uint32_t settleFamily; + std::uint32_t settleIndex; + std::uint32_t sparseFamily; + std::uint32_t sparseIndex; +}; + +void check(const WindowCase& test) { + const auto plan = AgcDriver::PlanDeviceQueues(test.families, 0, test.settles, true, true); + const bool expected = plan.mainCount == test.mainCount && plan.present == test.present && (!test.present || plan.presentIndex == test.presentIndex) && plan.settle == test.settle && (!test.settle || (plan.settleFamily == test.settleFamily && plan.settleIndex == test.settleIndex)) && plan.sparseFamily == test.sparseFamily && (test.sparseFamily == Own || plan.sparseIndex == test.sparseIndex); + Require(expected, std::string("device queue plan with a window, ") + test.name + ": got " + describe(plan)); } } @@ -62,6 +86,15 @@ int main() { {"only the settle queue binds", {family(Graphics, 1), family(VK_QUEUE_TRANSFER_BIT | Sparse, 1)}, true, 1, true, 1, 0, false, Own, 0}, }; for (const auto& test : cases) check(test); + const std::vector windowCases{ + {"one queue with a window", {family(Graphics | Sparse, 1)}, true, 1, false, 0, false, 0, 0, Own, 0}, + {"two queues, no settles, with a window", {family(Graphics, 2)}, false, 2, true, 1, false, 0, 0, Own, 0}, + {"two queues taken by the main and settle queues, with a window", {family(Graphics, 2)}, true, 2, false, 0, true, 0, 1, Own, 0}, + {"settle on the main family, three queues, with a window", {family(Graphics | Sparse, 3), family(VK_QUEUE_TRANSFER_BIT | Sparse, 1)}, true, 3, true, 2, true, 0, 1, 1, 0}, + {"one main queue and settle on another family, with a window", {family(Graphics, 1), family(VK_QUEUE_TRANSFER_BIT, 1)}, true, 1, false, 0, true, 1, 0, Own, 0}, + {"sparse-only family preferred, with a window", {family(Graphics | Sparse, 16), family(VK_QUEUE_TRANSFER_BIT | Sparse, 2), family(Sparse, 1)}, true, 3, true, 2, true, 0, 1, 2, 0}, + }; + for (const auto& test : windowCases) check(test); std::puts("device queue plan tests passed"); return 0; } catch (const std::exception& error) { From 008e3276b7fdf8498266f0adb64ef28cde7bad89 Mon Sep 17 00:00:00 2001 From: winstrex <79346005+winstrex@users.noreply.github.com> Date: Thu, 22 Oct 2026 10:43:20 -0300 Subject: [PATCH 130/428] docs(agc): describe where QueuePresent runs and record the single-queue fallback Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp | 6 ++++-- docs/dev/debt/libSceAgcDriver.md | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 45fa24b7ad..c2211da843 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -225,7 +225,7 @@ public: bool ProvokingVertexLast() const; bool GraphicsPipelineLibraries() const; // A presentation is a few steps so the presenter holds GuestMemory::GpuMutex only while it - // touches the queue. Presentations are slots (FlipInFlight() + 1, each with its own command + // touches the main queue. Presentations are slots (FlipInFlight() + 1, each with its own command // buffer, fence, kept resident image and dump buffer): RetirePresents(keep) (no mutex) retires // the slots whose fence signaled, oldest first, and waits for the oldest ones until at most // `keep` are in flight (the only CPU wait of the flip path; it returns the time spent waiting); @@ -234,7 +234,9 @@ public: // PresentClear/PresentDisplayBuffer record and submit the frame into the next slot (under the // mutex; the blit follows the frame's batches in queue order, so the displayed image is always // complete) and return whether one was submitted; QueuePresent hands the image to the swapchain - // (under the mutex). FinishPresent = RetirePresents(0): the render fence wait of the + // (on the device's presentation queue under that queue's mutex, with the GPU mutex released, + // when the presentation family had a free queue; otherwise on the main queue under the GPU + // mutex). FinishPresent = RetirePresents(0): the render fence wait of the // synchronous paths (APS5_FLIP_INFLIGHT=0, APS5_SYNC_FLIP, PresentPixels). AcquireImage returns // false when the swapchain is out of date (the frame is dropped; the next Resize recreates it); // a present without a prior AcquireImage acquires itself, one whose slot is in flight waits for diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 7569ec1c0f..a2a05c9e6d 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -116,6 +116,7 @@ - [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. On Windows, the NVIDIA driver (617.42) marks every page of a host import of private `MEM_WRITE_WATCH` memory written, asynchronously, until a GPU use of the import completes; a 4-byte read of it on the settle queue ends that even while main-queue work that used the import first is still running, and an earlier import of the same pages whose first use runs after that read marks none of them (`agc_import_settle_first_use`, `agc_import_settle_earlier_import`). Such an import stays watched only after a 4-byte read of it on the settle queue has completed inside the window. Each one waits for that read, which keeps its window open for one round trip on the settle queue, so a same-value CPU store from another thread in that time counts as unchanged too. An import made where no settle queue exists is compared instead. An import of under 1 MiB is not settled inside its window, because the settle blocks the importing thread and a title that imports a 64 KiB buffer around every submission lost about 19 % of its frame rate to that wait (Dripl1x, #6060). It is compared until its read, submitted in a batch by a later import lookup under `GpuMutex` while the imports match the registry, has completed; a later lookup then returns its whole blocks to the write watch, each stamped as a CPU store, so whatever was derived from them while they were compared reads as changed once. Its blocks stay compared while storage results are pending over them, and those of an import retired before that stay compared. - [Settled dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a dma-buf import of 1 MiB or more is read by a 4-byte copy on the settle queue right after it is made (`settleImport`), so the kernel maps its pages for the GPU there and not in the first submission that uses it (about 2 s for a 3 GiB import on RADV). The import's buffer is shared with the settle queue's family. An import made at map time settles on the guest thread that maps the range, inside its map call, so that thread waits while the kernel maps the pages: 0.47 to 2.2 s for the 3 GiB range of Hades (PPSA03355) and 0.49 to 4.3 s for the 4.5 GiB range of ASTRO BOT (PPSA21567) on RADV. Both titles map these ranges at start-up; a title that maps large ranges during gameplay stalls its mapping thread for as long. An import made from a submission settles under the GPU mutex, so its frame still pays for it; a smaller import, and every import on a device with no settle queue (one without a copy-capable queue besides the main one), pays in its first submission. A range imported through a host pointer, as lavapipe imports shared memory, is not settled - [Sparse dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a shared-backed range larger than one 64 MiB udmabuf buffer is imported as one sparse buffer bound to the 64 MiB buffers, on a queue of its own when the device has a second family that binds sparse memory. The path is decided before anything is imported, and a failure after that throws. A range whose offset in the backing file is not a multiple of 64 KiB starts its sparse buffer at the 64 KiB block below it (`HostImport::lead`). A range takes the single udmabuf import instead, which udmabuf's `size_limit_mb` refuses above 64 MiB by default so the range is copied, when the buffer's sparse block size is larger than 64 KiB (it is 64 KiB on RADV), when the sparse buffer does not accept every memory type a chunk import can have, when the backing file ends inside its last chunk, or when it is imported at map time on a device with no second family (the bind then needs the GPU mutex); the NVIDIA proprietary driver imports no dma-buf memory +- [Presentation under the GPU mutex](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) - on a device whose presentation family has no queue left after the main and settle queues, such as RADV, lavapipe and Intel's Windows driver (Arc Graphics on Meteor Lake, 101.9150), which expose a single graphics queue, `vkQueuePresentKHR` still runs on the main queue under `GuestMemory::GpuMutex`, so the draw committer and the queue workers wait for as long as the present blocks. How long that is on those drivers was not measured; on NVIDIA in FIFO mode it is about 6 ms per present. A device with a free queue in that family presents on it under its own mutex, after the GPU mutex is released. Closes when: a trace on a single-queue device shows no `GpuMutex.wait.*` behind `Vulkan.QueuePresent`. - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. - An [unnormalized S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) maps clamp mode 0 (repeat) on X and Y to clamp-to-edge as required by Vulkan unnormalized coordinates, and assigns the magnification filter when minification and magnification filters differ (since unnormalized coordinates sample at LOD 0). Anisotropy, TRUNC_COORD, MC_COORD_TRUNC and other clamp modes still throw. Secondary LOD bias (LOD_BIAS_SEC, bits [19:14]) is accepted and ignored, matching RDNA2 hardware measurements across explicit and implicit samples. High on Life (PPSA15388, #4736) uses secondary LOD bias and unnormalized coordinates with clamp mode 0 and differing min/mag filters during home base and city rendering. - [TRUNC_COORD](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) is not applied: the texel a point sample reads and the bilinear weights follow the host sampler's rounding. On RDNA 2 (RX 6700 XT), an S# with TRUNC_COORD 0 rounds the exact scaled coordinate to 8 fractional bits, ties to even, and one with TRUNC_COORD 1 truncates it, for point and bilinear filters. AMD's Windows driver (32.0.21045.5002) truncates for nearest samplers and rounds for linear ones. Under it, a TRUNC_COORD 0 point sample at most 1/512 texel below a texel edge reads the texel below the edge where the console reads the one above, and `k / width` computed in f32 lands there for about half of all k. A TRUNC_COORD 1 bilinear weight can be 1/256 above the console's. Other host drivers are not measured. From 873f92ea1d27244fb203f5a009119f7a3bfcf4c0 Mon Sep 17 00:00:00 2001 From: emirplus Date: Wed, 21 Oct 2026 22:48:06 +0300 Subject: [PATCH 131/428] fix(tools): decode snapshots with utf-8-sig and reconfigure streams in progress --- tools/progress.py | 8 ++++--- tools/tests/test_progress.py | 45 ++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/tools/progress.py b/tools/progress.py index 153975e4d5..06f18773b5 100644 --- a/tools/progress.py +++ b/tools/progress.py @@ -482,6 +482,10 @@ def validate_snapshot(snapshot): def main(argv=None): + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace") + if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8", errors="backslashreplace") global PRX, OPCODES previous = PRX, OPCODES parser = argparse.ArgumentParser() @@ -501,7 +505,7 @@ def main(argv=None): snapshots = [] for path in args.compare: try: - snapshots.append(json.loads(path.read_text(encoding="utf-8"))) + snapshots.append(json.loads(path.read_text(encoding="utf-8-sig"))) except (OSError, ValueError) as error: parser.error(f"{path}: {error}") for path, snapshot in zip(args.compare, snapshots): @@ -541,6 +545,4 @@ def main(argv=None): if __name__ == "__main__": - if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8") raise SystemExit(main()) diff --git a/tools/tests/test_progress.py b/tools/tests/test_progress.py index f018237531..edba4f7688 100644 --- a/tools/tests/test_progress.py +++ b/tools/tests/test_progress.py @@ -523,5 +523,50 @@ class ProgressTests(unittest.TestCase): self.assertIn("is not a source tree", stderr.getvalue()) self.assertIn(str(path), stderr.getvalue()) + def test_compare_accepts_snapshots_with_utf8_bom(self): + def snapshot(names): + return {"libraries": progress.summarize([ + {"name": "libScePad", "label": "Pad", "done": len(names), "todo": 0, + "done_names": names, "todo_names": [], "inert_names": []}]), + "shaders": {"done": 0, "total": 0, "percent": 0, "groups": [], "extra": []}} + with tempfile.TemporaryDirectory() as directory: + base = Path(directory) / "base.json" + head = Path(directory) / "head.json" + base.write_text("\ufeff" + json.dumps(snapshot([])), encoding="utf-8") + head.write_text("\ufeff" + json.dumps(snapshot(["Ready"])), encoding="utf-8") + stdout = io.StringIO() + with patch("sys.stdout", stdout): + code = progress.main(["--compare", str(base), str(head)]) + self.assertEqual(code, 0) + self.assertIn("implemented", stdout.getvalue()) + + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with patch.object(progress.sys, "stdout", out), patch.object(progress.sys, "stderr", err): + try: + progress.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + + if __name__ == "__main__": unittest.main() From 52487b7fcc58c2ead3d50b88af57072806918189 Mon Sep 17 00:00:00 2001 From: Gruzzberry Date: Wed, 21 Oct 2026 23:36:31 +0100 Subject: [PATCH 132/428] docs(debt): record the guest file names Windows does not create like FreeBSD Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libc.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ba3e5ca06c..be04566ade 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -122,6 +122,7 @@ - [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, decodes the input of the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`) on Linux as UTF-8, mirroring the narrow printf family, where FreeBSD uses the locale's `mbrtowc` (one byte per character in the C locale); the console's locale encoding is unverified. Input that is not UTF-8, a character outside the BMP, a scanset member outside ASCII, positional arguments next to a wide conversion and wide conversions in `fscanf` throw. Each character is stored as one 16-bit unit, widths and `_s` sizes count characters, and a `%lc` field cut short by the end of the input keeps what it read, as FreeBSD's `convert_wchar` does. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits - [The scanf family](../../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` - [Guest path resolution](../../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation.. A relinked module's `.guest.prx` is matched the same way when the original module is not beside it: Borderlands 4 (PPSA01494) loads `/app0/prx/libRenoirCore.PS5.prx`, which it ships as `librenoircore.ps5.prx`. A relinked module request that misses both the exact and the case-insensitive `.guest.prx` takes the sibling whose stem (the name without its last extension, ASCII case ignored) equals the requested stem and that ends with `.guest.prx`, shortest name first: Unity IL2CPP titles load `Media/Modules/.prx` and `Media/Plugins/.prx`, and the relinker names the converted module after the ELF it was built from, `.elf.guest.prx`. The console's module namespace is assumed to ignore case and the extension; several stem matches pick the shortest name instead of throwing, which is not measured on a console +- [Guest file names on Windows](../../../core/libs/prx/libc/src/General.cpp) (libc) reach the host unchanged, so names the host filesystem treats specially behave differently from FreeBSD, where only `/` and NUL are special. Measured on Windows 11 (10.0.26200), MinGW-w64 GCC 15.2.0 r7, by creating each name with `open` (`O_CREAT|O_EXCL|O_WRONLY`), writing one byte and listing the host directory: `a*b`, `a?b`, `ac`, `a|b` and `a"b` fail with `EINVAL` (22), where FreeBSD creates them. `trail.` and `trail ` create the host file `trail`, so the three names are one file here and three on FreeBSD. `NUL` succeeds and a reopen succeeds, but no file appears in the directory, so the write does not reach a regular file. `con`, `aux.txt` and `COM1` create ordinary files and are not affected. A fix needs a mapping between guest and host names, which does not exist. Colons are listed separately under Colons in guest names. Linux was not measured, and none of this was measured on a console. - [Colons in guest names](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows (NTFS) a guest name containing `:` is not a plain file name: `name:stream` opens or creates the alternate data stream `stream` of the file `name`. Creating `/savedata0/C:file` makes an empty file `C` whose stream `file` holds the data, and `stat` of `/savedata0/C` then reports a regular file. `mkdir` of a name containing `:`, such as `C:` or `dir:x`, fails with EINVAL. `name:stream` already behaved this way before drive-letter components were kept under their mount - [tzname](../../../core/libs/prx/libc/src/Time.cpp) (libc) points at the host's zone names, read at load and again by `tzset`: the abbreviations a `TZ` string gives (`EST5EDT` is `EST` and `EDT` on both hosts), but on Windows without `TZ` the CRT's full names such as `GMT Standard Time`, which ICU's [uprv_tzname](https://github.com/unicode-org/icu/blob/457157a92aa053e632cc7fcfd0e12f8a943b2d11/icu4c/source/common/putil.cpp#L1244-L1264) (ES-DE in PPSA99169) cannot map to a zone ID. A zone with no daylight-saving time has `tzname[1]` empty on Windows and equal to `tzname[0]` on Linux, where FreeBSD leaves three spaces. The console's names are not known - [localtime, mktime](../../../core/libs/prx/libc/src/Time.cpp) (libc) on Windows convert through the CRT's `localtime_s` and `mktime`, which accept about 1970 to January 3001 UTC and apply the CRT's daylight-saving rule. A time outside that range is converted a whole number of 400-year Gregorian cycles away, where the calendar repeats, so it gets the rule the CRT applies to that year and not the zone's historical one. On macOS `mktime` converts through the host's `mktime`, which refuses every year before 1900. A local time before 1900 is instead solved for directly: the offset `localtime_r` gives for a candidate time, which follows the zone's history (local mean time before its first transition), is subtracted from the local fields, and the step is repeated until the offset holds. `tm_isdst` is not consulted there, so a time with `tm_isdst` set where the zone had no daylight offset is taken as standard time. FreeBSD's [`time1`](https://github.com/freebsd/freebsd-src/blob/stable/11/contrib/tzcode/stdtime/localtime.c#L2036) would instead shift it by the difference between one of the zone's daylight types and a standard one. `gmtime` converts every time whose year fits an `int` on both hosts. From f2d3512d336bfff5e72b75d46c613d773188d44b Mon Sep 17 00:00:00 2001 From: Gruzzberry Date: Wed, 21 Oct 2026 23:36:32 +0100 Subject: [PATCH 133/428] docs(debt): say the guest file names are not escaped rather than unchanged Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index be04566ade..a4a3b00295 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -122,7 +122,7 @@ - [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, decodes the input of the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`) on Linux as UTF-8, mirroring the narrow printf family, where FreeBSD uses the locale's `mbrtowc` (one byte per character in the C locale); the console's locale encoding is unverified. Input that is not UTF-8, a character outside the BMP, a scanset member outside ASCII, positional arguments next to a wide conversion and wide conversions in `fscanf` throw. Each character is stored as one 16-bit unit, widths and `_s` sizes count characters, and a `%lc` field cut short by the end of the input keeps what it read, as FreeBSD's `convert_wchar` does. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits - [The scanf family](../../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` - [Guest path resolution](../../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation.. A relinked module's `.guest.prx` is matched the same way when the original module is not beside it: Borderlands 4 (PPSA01494) loads `/app0/prx/libRenoirCore.PS5.prx`, which it ships as `librenoircore.ps5.prx`. A relinked module request that misses both the exact and the case-insensitive `.guest.prx` takes the sibling whose stem (the name without its last extension, ASCII case ignored) equals the requested stem and that ends with `.guest.prx`, shortest name first: Unity IL2CPP titles load `Media/Modules/.prx` and `Media/Plugins/.prx`, and the relinker names the converted module after the ELF it was built from, `.elf.guest.prx`. The console's module namespace is assumed to ignore case and the extension; several stem matches pick the shortest name instead of throwing, which is not measured on a console -- [Guest file names on Windows](../../../core/libs/prx/libc/src/General.cpp) (libc) reach the host unchanged, so names the host filesystem treats specially behave differently from FreeBSD, where only `/` and NUL are special. Measured on Windows 11 (10.0.26200), MinGW-w64 GCC 15.2.0 r7, by creating each name with `open` (`O_CREAT|O_EXCL|O_WRONLY`), writing one byte and listing the host directory: `a*b`, `a?b`, `ac`, `a|b` and `a"b` fail with `EINVAL` (22), where FreeBSD creates them. `trail.` and `trail ` create the host file `trail`, so the three names are one file here and three on FreeBSD. `NUL` succeeds and a reopen succeeds, but no file appears in the directory, so the write does not reach a regular file. `con`, `aux.txt` and `COM1` create ordinary files and are not affected. A fix needs a mapping between guest and host names, which does not exist. Colons are listed separately under Colons in guest names. Linux was not measured, and none of this was measured on a console. +- [Guest file names on Windows](../../../core/libs/prx/libc/src/General.cpp) (libc) are not escaped or mapped before the host call, so names the host filesystem treats specially behave differently from FreeBSD, where only `/` and NUL are special. Measured on Windows 11 (10.0.26200), MinGW-w64 GCC 15.2.0 r7, by creating each name with `open` (`O_CREAT|O_EXCL|O_WRONLY`), writing one byte and listing the host directory: `a*b`, `a?b`, `ac`, `a|b` and `a"b` fail with `EINVAL` (22), where FreeBSD creates them. `trail.` and `trail ` create the host file `trail`, so the three names are one file here and three on FreeBSD. `NUL` succeeds and a reopen succeeds, but no file appears in the directory, so the write does not reach a regular file. `con`, `aux.txt` and `COM1` create ordinary files and are not affected. A fix needs a mapping between guest and host names, which does not exist. Colons are listed separately under Colons in guest names. Linux was not measured, and none of this was measured on a console. - [Colons in guest names](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows (NTFS) a guest name containing `:` is not a plain file name: `name:stream` opens or creates the alternate data stream `stream` of the file `name`. Creating `/savedata0/C:file` makes an empty file `C` whose stream `file` holds the data, and `stat` of `/savedata0/C` then reports a regular file. `mkdir` of a name containing `:`, such as `C:` or `dir:x`, fails with EINVAL. `name:stream` already behaved this way before drive-letter components were kept under their mount - [tzname](../../../core/libs/prx/libc/src/Time.cpp) (libc) points at the host's zone names, read at load and again by `tzset`: the abbreviations a `TZ` string gives (`EST5EDT` is `EST` and `EDT` on both hosts), but on Windows without `TZ` the CRT's full names such as `GMT Standard Time`, which ICU's [uprv_tzname](https://github.com/unicode-org/icu/blob/457157a92aa053e632cc7fcfd0e12f8a943b2d11/icu4c/source/common/putil.cpp#L1244-L1264) (ES-DE in PPSA99169) cannot map to a zone ID. A zone with no daylight-saving time has `tzname[1]` empty on Windows and equal to `tzname[0]` on Linux, where FreeBSD leaves three spaces. The console's names are not known - [localtime, mktime](../../../core/libs/prx/libc/src/Time.cpp) (libc) on Windows convert through the CRT's `localtime_s` and `mktime`, which accept about 1970 to January 3001 UTC and apply the CRT's daylight-saving rule. A time outside that range is converted a whole number of 400-year Gregorian cycles away, where the calendar repeats, so it gets the rule the CRT applies to that year and not the zone's historical one. On macOS `mktime` converts through the host's `mktime`, which refuses every year before 1900. A local time before 1900 is instead solved for directly: the offset `localtime_r` gives for a candidate time, which follows the zone's history (local mean time before its first transition), is subtracted from the local fields, and the step is repeated until the offset holds. `tm_isdst` is not consulted there, so a time with `tm_isdst` set where the zone had no daylight offset is taken as standard time. FreeBSD's [`time1`](https://github.com/freebsd/freebsd-src/blob/stable/11/contrib/tzcode/stdtime/localtime.c#L2036) would instead shift it by the difference between one of the zone's daylight types and a standard one. `gmtime` converts every time whose year fits an `int` on both hosts. From d2639b06c75bebbf3f0df1e04e157033ba1f0a16 Mon Sep 17 00:00:00 2001 From: Gruzzberry Date: Wed, 21 Oct 2026 23:36:32 +0100 Subject: [PATCH 134/428] docs(debt): shorten the guest file names entry to state the gap first Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index a4a3b00295..35330c91a4 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -122,7 +122,7 @@ - [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, decodes the input of the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`) on Linux as UTF-8, mirroring the narrow printf family, where FreeBSD uses the locale's `mbrtowc` (one byte per character in the C locale); the console's locale encoding is unverified. Input that is not UTF-8, a character outside the BMP, a scanset member outside ASCII, positional arguments next to a wide conversion and wide conversions in `fscanf` throw. Each character is stored as one 16-bit unit, widths and `_s` sizes count characters, and a `%lc` field cut short by the end of the input keeps what it read, as FreeBSD's `convert_wchar` does. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits - [The scanf family](../../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` - [Guest path resolution](../../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation.. A relinked module's `.guest.prx` is matched the same way when the original module is not beside it: Borderlands 4 (PPSA01494) loads `/app0/prx/libRenoirCore.PS5.prx`, which it ships as `librenoircore.ps5.prx`. A relinked module request that misses both the exact and the case-insensitive `.guest.prx` takes the sibling whose stem (the name without its last extension, ASCII case ignored) equals the requested stem and that ends with `.guest.prx`, shortest name first: Unity IL2CPP titles load `Media/Modules/.prx` and `Media/Plugins/.prx`, and the relinker names the converted module after the ELF it was built from, `.elf.guest.prx`. The console's module namespace is assumed to ignore case and the extension; several stem matches pick the shortest name instead of throwing, which is not measured on a console -- [Guest file names on Windows](../../../core/libs/prx/libc/src/General.cpp) (libc) are not escaped or mapped before the host call, so names the host filesystem treats specially behave differently from FreeBSD, where only `/` and NUL are special. Measured on Windows 11 (10.0.26200), MinGW-w64 GCC 15.2.0 r7, by creating each name with `open` (`O_CREAT|O_EXCL|O_WRONLY`), writing one byte and listing the host directory: `a*b`, `a?b`, `ac`, `a|b` and `a"b` fail with `EINVAL` (22), where FreeBSD creates them. `trail.` and `trail ` create the host file `trail`, so the three names are one file here and three on FreeBSD. `NUL` succeeds and a reopen succeeds, but no file appears in the directory, so the write does not reach a regular file. `con`, `aux.txt` and `COM1` create ordinary files and are not affected. A fix needs a mapping between guest and host names, which does not exist. Colons are listed separately under Colons in guest names. Linux was not measured, and none of this was measured on a console. +- [Guest file names on Windows](../../../core/libs/prx/libc/src/General.cpp) (libc) are not escaped or mapped before the host call, so names Windows treats specially differ from FreeBSD, where only `/` and NUL are special. `a*b`, `a?b`, `ac`, `a|b` and `a"b` fail with `EINVAL` (22) where FreeBSD creates them. `trail.` and `trail ` both create the host file `trail`, so three FreeBSD names are one file here. `NUL` and a reopen of it succeed, but no file is created. `con`, `aux.txt` and `COM1` create ordinary files. Colons are under Colons in guest names. Measured on Windows 11 (10.0.26200), MinGW-w64 GCC 15.2.0 r7, with `open` (`O_CREAT|O_EXCL|O_WRONLY`), a one-byte write and a listing of the host directory; not measured on Linux or a console. A fix needs a guest-to-host name mapping. - [Colons in guest names](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows (NTFS) a guest name containing `:` is not a plain file name: `name:stream` opens or creates the alternate data stream `stream` of the file `name`. Creating `/savedata0/C:file` makes an empty file `C` whose stream `file` holds the data, and `stat` of `/savedata0/C` then reports a regular file. `mkdir` of a name containing `:`, such as `C:` or `dir:x`, fails with EINVAL. `name:stream` already behaved this way before drive-letter components were kept under their mount - [tzname](../../../core/libs/prx/libc/src/Time.cpp) (libc) points at the host's zone names, read at load and again by `tzset`: the abbreviations a `TZ` string gives (`EST5EDT` is `EST` and `EDT` on both hosts), but on Windows without `TZ` the CRT's full names such as `GMT Standard Time`, which ICU's [uprv_tzname](https://github.com/unicode-org/icu/blob/457157a92aa053e632cc7fcfd0e12f8a943b2d11/icu4c/source/common/putil.cpp#L1244-L1264) (ES-DE in PPSA99169) cannot map to a zone ID. A zone with no daylight-saving time has `tzname[1]` empty on Windows and equal to `tzname[0]` on Linux, where FreeBSD leaves three spaces. The console's names are not known - [localtime, mktime](../../../core/libs/prx/libc/src/Time.cpp) (libc) on Windows convert through the CRT's `localtime_s` and `mktime`, which accept about 1970 to January 3001 UTC and apply the CRT's daylight-saving rule. A time outside that range is converted a whole number of 400-year Gregorian cycles away, where the calendar repeats, so it gets the rule the CRT applies to that year and not the zone's historical one. On macOS `mktime` converts through the host's `mktime`, which refuses every year before 1900. A local time before 1900 is instead solved for directly: the offset `localtime_r` gives for a candidate time, which follows the zone's history (local mean time before its first transition), is subtracted from the local fields, and the step is repeated until the offset holds. `tm_isdst` is not consulted there, so a time with `tm_isdst` set where the zone had no daylight offset is taken as standard time. FreeBSD's [`time1`](https://github.com/freebsd/freebsd-src/blob/stable/11/contrib/tzcode/stdtime/localtime.c#L2036) would instead shift it by the difference between one of the zone's daylight types and a standard one. `gmtime` converts every time whose year fits an `int` on both hosts. From 3e96c132171f8d3ebc3a70d0f9311cbba27b8faf Mon Sep 17 00:00:00 2001 From: Gruzzberry Date: Wed, 21 Oct 2026 23:58:45 +0100 Subject: [PATCH 135/428] fix(libc): follow a Windows symbolic link in access access read the attributes and the security descriptor of the link itself on Windows, so a dangling link passed F_OK and R_OK and a link to a read-only file passed W_OK. A reparse point is now opened through its target and the target's path is checked, as FreeBSD 11.4's kern_accessat looks the path up with FOLLOW: a missing target fails with ENOENT and a link loop with ELOOP. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Filesystem.cpp | 34 +++++++++++++++++++--- core/libs/tests/GuestAccess.cpp | 41 +++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/src/Filesystem.cpp b/core/libs/prx/libc/src/Filesystem.cpp index 510477eb7a..c2fbebd7d8 100644 --- a/core/libs/prx/libc/src/Filesystem.cpp +++ b/core/libs/prx/libc/src/Filesystem.cpp @@ -11,6 +11,30 @@ #include #endif +#ifdef _WIN32 +static std::filesystem::path FollowedPath(const std::filesystem::path& path) { + const DWORD attributes = GetFileAttributesW(path.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return path; + const HANDLE handle = CreateFileW(path.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) return {}; + std::wstring target(MAX_PATH, L'\0'); + DWORD length = GetFinalPathNameByHandleW(handle, target.data(), static_cast(target.size()), FILE_NAME_NORMALIZED); + if (length >= target.size()) { + target.resize(length); + length = GetFinalPathNameByHandleW(handle, target.data(), length, FILE_NAME_NORMALIZED); + } + const DWORD error = GetLastError(); + CloseHandle(handle); + if (length == 0 || length >= target.size()) { + SetLastError(length == 0 ? error : ERROR_FILENAME_EXCED_RANGE); + return {}; + } + target.resize(length); + return target; +} +#endif + extern "C" int APS5_VABI access_nid_postfix(const char* path, int mode) { if (mode < 0 || (mode & ~7)) { errno = 22; return -1; } if (!path) { errno = 14; return -1; } @@ -18,9 +42,11 @@ extern "C" int APS5_VABI access_nid_postfix(const char* path, int mode) { try { const auto resolved = ResolvePath_nid_no_patch(path); #ifdef _WIN32 - const DWORD attributes = GetFileAttributesW(resolved.c_str()); + const auto target = FollowedPath(resolved); + const DWORD attributes = target.empty() ? INVALID_FILE_ATTRIBUTES : GetFileAttributesW(target.c_str()); if (attributes == INVALID_FILE_ATTRIBUTES) { - errno = FilesystemError(std::error_code(static_cast(GetLastError()), std::system_category())); + const DWORD failure = GetLastError(); + errno = failure == ERROR_CANT_RESOLVE_FILENAME ? 62 : FilesystemError(std::error_code(static_cast(failure), std::system_category())); if (errno == 2) errno = NotFoundError_nid_no_patch(resolved); return -1; } @@ -30,10 +56,10 @@ extern "C" int APS5_VABI access_nid_postfix(const char* path, int mode) { } DWORD size = 0; constexpr auto information = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION; - GetFileSecurityW(resolved.c_str(), information, nullptr, 0, &size); + GetFileSecurityW(target.c_str(), information, nullptr, 0, &size); if (!size) { errno = 13; return -1; } std::vector descriptor(size); - if (!GetFileSecurityW(resolved.c_str(), information, descriptor.data(), size, &size)) { + if (!GetFileSecurityW(target.c_str(), information, descriptor.data(), size, &size)) { errno = 13; return -1; } struct Token { diff --git a/core/libs/tests/GuestAccess.cpp b/core/libs/tests/GuestAccess.cpp index f4fa4fb8e8..b55facbffa 100644 --- a/core/libs/tests/GuestAccess.cpp +++ b/core/libs/tests/GuestAccess.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/General.hpp" #include +#include #include #include #ifdef _WIN32 @@ -76,6 +77,46 @@ int main() { Require(access_nid_postfix("sample.txt", 1) == -1 && *__error_nid_postfix() == 13); std::filesystem::permissions(file, std::filesystem::perms::owner_exec, std::filesystem::perm_options::add); Require(access_nid_postfix("sample.txt", 1) == 0); +#endif + const auto dangling = directory / "dangling.link"; + const auto pointing = directory / "sample.link"; + const auto loop = directory / "loop.link"; +#ifdef _WIN32 + const DWORD linkFlags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; + const bool linked = CreateSymbolicLinkW(dangling.c_str(), L"missing.bin", linkFlags) && + CreateSymbolicLinkW(pointing.c_str(), L"sample.txt", linkFlags) && CreateSymbolicLinkW(loop.c_str(), L"loop.link", linkFlags); +#else + std::error_code linkError; + std::filesystem::create_symlink("missing.bin", dangling, linkError); + if (!linkError) std::filesystem::create_symlink("sample.txt", pointing, linkError); + if (!linkError) std::filesystem::create_symlink("loop.link", loop, linkError); + const bool linked = !linkError; +#endif + if (linked) { + Require(access_nid_postfix("dangling.link", 0) == -1 && *__error_nid_postfix() == 2); + Require(access_nid_postfix("dangling.link", 4) == -1 && *__error_nid_postfix() == 2); + Require(access_nid_postfix("loop.link", 0) == -1 && *__error_nid_postfix() == 62); + Require(access_nid_postfix("sample.link", 6) == 0); +#ifdef _WIN32 + Require(SetFileAttributesW(file.c_str(), FILE_ATTRIBUTE_READONLY)); + Require(access_nid_postfix("sample.link", 4) == 0); + Require(access_nid_postfix("sample.link", 2) == -1 && *__error_nid_postfix() == 13); + Require(SetFileAttributesW(file.c_str(), FILE_ATTRIBUTE_NORMAL)); + Require(SetFileAttributesW(pointing.c_str(), FILE_ATTRIBUTE_READONLY)); + Require(access_nid_postfix("sample.link", 2) == 0); + Require(SetFileAttributesW(pointing.c_str(), FILE_ATTRIBUTE_NORMAL)); +#endif + } else { + std::fprintf(stderr, "symbolic links unavailable: the access link cases not tested\n"); + } +#ifdef _WIN32 + DeleteFileW(dangling.c_str()); + DeleteFileW(pointing.c_str()); + DeleteFileW(loop.c_str()); +#else + std::filesystem::remove(dangling, linkError); + std::filesystem::remove(pointing, linkError); + std::filesystem::remove(loop, linkError); #endif Require(chdir_nid_postfix("/") == 0); std::filesystem::remove(file); From 4e5db13927be1e517667d1a76c3de8069086030a Mon Sep 17 00:00:00 2001 From: Gruzzberry Date: Thu, 22 Oct 2026 00:00:50 +0100 Subject: [PATCH 136/428] fix(libkernel): follow a Windows symbolic link in chmod _wchmod changes the read-only attribute of the path it is given, so on Windows chmod through a symbolic link changed the link and left the file it names unchanged, and a dangling link succeeded. A reparse point is now opened through its target and the target's path is changed, as FreeBSD 11.4's sys_chmod passes FOLLOW: a missing target fails with ENOENT and a link loop with ELOOP. The path lookup of a handle moves out of WindowsDescriptorPath_nid_no_patch so both use it. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/File/src/Stdio.cpp | 37 ++++++++++++++++------ core/libs/tests/GuestFilesystem.cpp | 31 ++++++++++++++++++ 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index b00d71b5d4..0dff46deab 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -91,16 +91,7 @@ static int NativeMkdir(const std::filesystem::path& path, std::uint16_t mode) { (void)mode; return ::_wmkdir(path.wstring().c_str()); } -static int NativeChmod(const std::filesystem::path& path, int mode) { - RecordWrittenPath_nid_no_patch(path); - return ::_wchmod(path.wstring().c_str(), mode); -} -std::optional File::WindowsDescriptorPath_nid_no_patch(int descriptor) { - const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); - if (handle == INVALID_HANDLE_VALUE) { - errno = EBADF; - return std::nullopt; - } +static std::optional HandlePath(HANDLE handle) { std::wstring path(MAX_PATH, L'\0'); auto length = ::GetFinalPathNameByHandleW(handle, path.data(), static_cast(path.size()), FILE_NAME_NORMALIZED); if (length >= path.size()) { @@ -120,6 +111,32 @@ std::optional File::WindowsDescriptorPath_nid_no_patch(in else if (path.starts_with(LR"(\\?\)") && path.size() > 6 && path[5] == L':' && path[6] == L'\\') path.erase(0, 4); return path; } +std::optional File::WindowsDescriptorPath_nid_no_patch(int descriptor) { + const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); + if (handle == INVALID_HANDLE_VALUE) { + errno = EBADF; + return std::nullopt; + } + return HandlePath(handle); +} +static int NativeChmod(const std::filesystem::path& path, int mode) { + const DWORD attributes = GetFileAttributesW(path.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) { + RecordWrittenPath_nid_no_patch(path); + return ::_wchmod(path.wstring().c_str(), mode); + } + const HANDLE handle = CreateFileW(path.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) { + errno = WindowsOpenError_nid_no_patch(GetLastError()); + return -1; + } + const auto target = HandlePath(handle); + CloseHandle(handle); + if (!target) return -1; + RecordWrittenPath_nid_no_patch(*target); + return ::_wchmod(target->wstring().c_str(), mode); +} static int NativeFchmod(int descriptor, int mode) { const auto path = File::WindowsDescriptorPath_nid_no_patch(descriptor); return path ? NativeChmod(*path, mode) : -1; diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 86b6b6c3e8..cb50a38167 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -914,6 +914,37 @@ int main() { }; Require(sceKernelFchmod(descriptor, 0400) == 0 && !ownerWrite()); Require(fchmod_nid_postfix(descriptor, 0600) == 0 && ownerWrite()); + const auto sizedLink = sized.parent_path() / "sized.link"; + const auto missingLink = sized.parent_path() / "missing.link"; + const auto loopLink = sized.parent_path() / "loop.link"; +#ifdef _WIN32 + const DWORD linkFlags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; + const bool chmodLinked = CreateSymbolicLinkW(sizedLink.wstring().c_str(), sized.filename().wstring().c_str(), linkFlags) && + CreateSymbolicLinkW(missingLink.wstring().c_str(), L"missing.bin", linkFlags) && + CreateSymbolicLinkW(loopLink.wstring().c_str(), L"loop.link", linkFlags); +#else + std::error_code chmodLinkError; + std::filesystem::create_symlink(sized.filename(), sizedLink, chmodLinkError); + if (!chmodLinkError) std::filesystem::create_symlink("missing.bin", missingLink, chmodLinkError); + if (!chmodLinkError) std::filesystem::create_symlink("loop.link", loopLink, chmodLinkError); + const bool chmodLinked = !chmodLinkError; +#endif + if (chmodLinked) { + Require(chmod_nid_postfix(sizedLink.string().c_str(), 0400) == 0 && !ownerWrite()); +#ifdef _WIN32 + Require((GetFileAttributesW(sizedLink.wstring().c_str()) & FILE_ATTRIBUTE_READONLY) == 0); +#endif + Require(chmod_nid_postfix(sizedLink.string().c_str(), 0600) == 0 && ownerWrite()); + Require(chmod_nid_postfix(missingLink.string().c_str(), 0600) == -1 && *__error_nid_postfix() == 2); +#ifdef _WIN32 + Require(chmod_nid_postfix(loopLink.string().c_str(), 0600) == -1 && *__error_nid_postfix() == 62); +#endif + } else { + std::fprintf(stderr, "symbolic links unavailable: the chmod link cases not tested\n"); + } + unlink_nid_postfix(sizedLink.string().c_str()); + unlink_nid_postfix(missingLink.string().c_str()); + unlink_nid_postfix(loopLink.string().c_str()); Require(sceKernelFtruncate(descriptor, 3) == 0); FileStat times{}; const KernelTimeval past[2]{{1000000000, 0}, {1000000000, 500000}}; From 3cd6a37bfb9980c9a5f3b2635124a25c44c4ade7 Mon Sep 17 00:00:00 2001 From: Gruzzberry Date: Thu, 22 Oct 2026 00:16:21 +0100 Subject: [PATCH 137/428] fix(libkernel): fail open with O_CREAT and O_EXCL on a Windows symbolic link FreeBSD 11.4's vn_open_cred does not follow the last component when O_EXCL is set, so O_CREAT|O_EXCL on a symbolic link fails with EEXIST whatever it points to. On Windows the host open followed a dangling link and created its target. A reparse point in the last component now fails O_CREAT|O_EXCL with EEXIST before the host open. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/File/src/Open.cpp | 4 ++++ core/libs/tests/GuestFilesystem.cpp | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 9b79078010..49a2d405a0 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -212,6 +212,10 @@ static int OpenNative(const std::filesystem::path& native, int flags, std::uint1 const auto status = std::filesystem::status(native, error); if (std::filesystem::exists(status) && !std::filesystem::is_directory(status)) return SceErrorFromErrno(ENOTDIR); } + if ((flags & (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL)) == (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL) && native.has_filename()) { + const DWORD attributes = GetFileAttributesW(native.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) return SceErrorFromErrno(EEXIST); + } #endif if ((flags & SCE_KERNEL_O_NOFOLLOW) != 0 && (flags & (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL)) != (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL)) { constexpr int GuestEmlink = 31; diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 86b6b6c3e8..390c634aff 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -718,6 +718,27 @@ int main() { Require(fchflags_nid_postfix(-1, 0) == -1 && *__error_nid_postfix() == 9); const int flaggedRoot = sceKernelOpen(root.string().c_str(), 0x20000, 0); Require(flaggedRoot >= 0 && fchflags_nid_postfix(flaggedRoot, 0) == 0 && sceKernelClose(flaggedRoot) == 0); + const auto exclusiveLink = root / "exclusive.link"; + const auto exclusiveTarget = root / "exclusive-target.txt"; +#ifdef _WIN32 + const bool exclusiveLinked = CreateSymbolicLinkW(exclusiveLink.wstring().c_str(), L"exclusive-target.txt", + SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0; +#else + std::error_code exclusiveLinkError; + std::filesystem::create_symlink("exclusive-target.txt", exclusiveLink, exclusiveLinkError); + const bool exclusiveLinked = !exclusiveLinkError; +#endif + if (exclusiveLinked) { + Require(open_nid_postfix(exclusiveLink.string().c_str(), 0xa01, 0644) == -1 && *__error_nid_postfix() == 17); + Require(sceKernelOpen(exclusiveLink.string().c_str(), 0xa01, 0644) == static_cast(0x80020011u)); + Require(!std::filesystem::exists(exclusiveTarget)); + const int exclusiveCreated = open_nid_postfix(exclusiveLink.string().c_str(), 0x201, 0644); + Require(exclusiveCreated >= 0 && close_nid_postfix(exclusiveCreated) == 0 && std::filesystem::is_regular_file(exclusiveTarget)); + Require(open_nid_postfix(exclusiveLink.string().c_str(), 0xa01, 0644) == -1 && *__error_nid_postfix() == 17); + Require(unlink_nid_postfix(exclusiveLink.string().c_str()) == 0 && unlink_nid_postfix(exclusiveTarget.string().c_str()) == 0); + } else { + std::fprintf(stderr, "symbolic links unavailable: the O_EXCL link cases not tested\n"); + } int flaggedPipe[2]; Require(pipe_nid_postfix(flaggedPipe) == 0); Require(fchflags_nid_postfix(flaggedPipe[0], 0) == -1 && *__error_nid_postfix() == 22); From f2e4290e9c75880fc964f56528414713d2e21e96 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 23 Oct 2026 07:36:05 -0700 Subject: [PATCH 138/428] docs(debt): record piece-split ranges over the host import budget --- docs/dev/debt/libSceAgcDriver.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 4d371aa77d..0881cb6203 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -220,6 +220,7 @@ - [Depth surfaces created over storage writes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth surface created without HTILE copies its depth plane from the storage image at the plane's address, a pending one first, when that image has the plane's extent, one level, a 2D view or a 2D array view (its layer 0 is copied) and a 16-bit format for a 16-bit plane or a 32-bit format for a 32-bit plane. A multisampled surface, a surface the device stores in another format, a surface with HTILE and a surface without such an image start from `DB_DEPTH_CLEAR`, and the stencil plane from `DB_STENCIL_CLEAR`; CPU writes to the plane's memory are not read. Not measured on the console, where the plane is that memory. - [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does, but only on a queue that has never delivered an interrupt from such a release: the queue is latched the first time a release interrupt reaches the event queue, and flips on that queue deliver nothing while its own release interrupts keep arriving. The latch is cleared when the queue has flipped five times without another interrupt from its own releases: the fifth flip delivers the event and unlatched the queue, so a title that waits on the flip event after latching recovers only if it submits further flips. The bound cannot be by time: the next release interrupt can arrive seconds later than a suppressed flip across loading stalls, and delivering an unexpected flip event on a queue that pairs flips with releases can crash; no fixed window short enough to unblock a waiting title keeps such a title alive. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; nor is how the console behaves for a title that waits on the flip event and also raises release interrupts on the same queue. - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less the larger of 2 GiB and half that heap (imports, type-3 mirrors and buffers and textures placed in host memory share it; on a 16 GiB machine whose host heap is half the memory, a 2 GiB reserve left too little for the rest). Where the device enables `VK_EXT_memory_budget`, that host heap's reported budget less its other usage and an eighth, in 64 MiB steps, read at most once a second, also bounds the default budget once an import's `vkAllocateMemory` has failed; a set `APS5_HOST_IMPORT_MIB` is never lowered by the report. The report's other usage includes the host-visible copies and heap mirrors that stand in for refused imports, so it grows with the refusals; the first time the report would lower the budget, `[gpu] host import budget: host heap ...` prints the heap's reported budget and usage and the live imports. An import's `vkAllocateMemory` failure within an eighth of the budget, or the third in a row, lowers the budget to the live imports until the device or the configured budget changes; an isolated failure further below only marks its range failed. Live imports are not capped by `maxMemoryAllocationCount`: NVIDIA's Windows driver reports 4096, while Village held 27,167 live imports without a failure. A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. An address-based build does not evict either, except for a range the budget refused (or remembers refusing) and no mirror serves whose copy would take the build's copies past `APS5_ADDRESS_COPY_MAX_MIB`. A heap mirror, sparse or not, whose range has left the write watch is refreshed in full at each use, every block counted as written; a build plans its range again only once the import path takes the range. A request that may not evict (an address-based build, an import when a range is mapped) remembers a refused range or block, with the live bytes and the budget at the refusal, and is answered from that memory, still counted as refused, until live imports drop below those bytes, the budget changes or one of its ranges changes. A request that may evict remembers its refusal too, answering later requests for the same run from memory with the room the refusal left (a request whose range fits in that room steps down to a smaller block without scanning the imports again, and a refusal that stepped down is remembered for each block it tried), while no import was made or retired, the budget has not grown, the import window and the run are unchanged, and no import can have become evictable since: the use count stays below the first use at which an import that was neither evictable nor held could leave the 256-lookup protection, and either the build floor has not risen or no held import could leave it yet. A join remembers its refusal the same way, apart from other requests for the same run, so a buffer bound across refused ranges does not try the join again at every upload. The shutdown line `[gpu] host imports at shutdown` gives the live, made, refused (and remembered) and failed counts per requester. The 64 MiB window, the 256-lookup protection, the 2 GiB reserve, the eighth, the 64 MiB steps and the three failures are choices, not measured. +- [Piece-split ranges over the host import budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): an address-based build imports a registered range larger than the device's largest allocation in pieces. If the budget refuses a piece, the range is mirrored or copied without the evicting retry other refused ranges get when their copy would pass `APS5_ADDRESS_COPY_MAX_MIB`, so such a copy can still exceed that cap and end the build. - [Device-local staging](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) - a written or atomic buffer element inside a host import, including one an address-based compute build reaches through its pointers, is copied to device memory before the dispatch and copied back whole after it, so a CPU store into that range in between is rolled back. - [Periodic pipeline cache saves skip busy intervals](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp) (libSceAgcDriver) - the saver thread snapshots the Vulkan pipeline cache only when no pipeline creation holds it (`TryLockForSnapshot`); otherwise it skips that 10 s interval. A blocking snapshot queued behind a long creation (an NVIDIA compile of a 1.5M-word compute pipeline runs for minutes) makes the writer-preferring `shared_mutex` hold every later pipeline creation on every queue until that compile ends, which froze Yōtei's (PPSA26344) loading at frame 797-799 for the whole compile. While creations overlap without a gap, no periodic save happens, so a crash in that stretch loses the pipelines built since the last save; `SaveNow` and the teardown save still wait for the lock. How often busy stretches skip saves in practice is not measured. - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. From 9e6bd365f662d26ee5be256c41949443c8c560b8 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Thu, 22 Oct 2026 02:20:33 +0700 Subject: [PATCH 139/428] docs(libkernel): drop stale mlockall, open file limit and protection bit claims Co-Authored-By: Claude --- docs/dev/debt/libkernel.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 515c8d8c2b..59e33e476f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -112,8 +112,7 @@ - [qk043QPsrZU](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature - [uDGXwUKqi8s](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature - [sceKernelGetCpuTemperature, sceKernelGetSocSensorTemperature](../../../core/libs/prx/libkernel/System/src/ThermalSensors.cpp) (libkernel) - read the host's thermal sensors: on Linux from `/sys/class/hwmon` and `/sys/class/thermal`, on Windows from the ACPI thermal zones (`MSAcpi_ThermalZoneTemperature` over WMI) and the storage temperature property of the first physical drive. Sensor 0 is the CPU/package value, 1 the first other value and 2 an NVMe value; a host that exposes none reports every sensor as unsupported (`EINVAL`). The console's SoC channel count and meanings are hardware-dependent and differ from the host's, the host sensors are not the console's, the ACPI reading is unavailable on many machines, and the storage temperature property is numbered 52 on Windows 10 2004 and later and 22 before it, so the mapping, the values and the unsupported-input error are unverified; the storage channel was checked against the drive's own SMART composite temperature on one NVMe host (`StorageDeviceTemperatureProperty`, 36 °C on a WD_BLACK SN7100). `sceKernelGetCpuTemperature` reports sensor 0. PPSA99058 and PPSA99420 call both -- [LinuxProtFromSce](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - protection bits 0x100 and 0x200 are accepted and assumed not to affect host CPU access; host protection is derived only from the read, write and execute bits -- [getdtablesize](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - returns the smaller of the `RLIMIT_NOFILE` soft limit of `getrlimit` and the host's `getdtablesize()`, capped at `INT_MAX`: FreeBSD returns the smaller of the `RLIMIT_NOFILE` soft limit and `maxfilesperproc` ([`sys_getdtablesize`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L338-L355)), and the host call supplies that bound on macOS, where the soft limit alone is not one (`setrlimit` accepts soft limits above `maxfilesperproc`, `RLIM_INFINITY` included); `maxfilesperproc` is not measured on the console. The guest limit is the host's soft limit, raised to the hard limit when libkernel loads only on Linux: `core/libs/prx/libkernel/File/src/OpenFileLimit.cpp` is `__linux__`-only, so on macOS the guest gets whatever soft limit the host process inherited. VLC (PPSA85300) imports it for a `closefrom` loop in the child of its spawn helper ([libc_stubs.c](https://github.com/ZiZc3/VLC/blob/d46be6a68f4244466b041c2dc1b640915ac1a983/ps5/compat/libc_stubs.c#L66-L72)), which never runs because its `fork` fails with `ENOSYS` +- [getdtablesize](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - returns the smaller of the `RLIMIT_NOFILE` soft limit of `getrlimit` and the host's `getdtablesize()`, capped at `INT_MAX`: FreeBSD returns the smaller of the `RLIMIT_NOFILE` soft limit and `maxfilesperproc` ([`sys_getdtablesize`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L338-L355)), and the host call supplies that bound on macOS, where the soft limit alone is not one (`setrlimit` accepts soft limits above `maxfilesperproc`, `RLIM_INFINITY` included); `maxfilesperproc` is not measured on the console. On Linux and macOS the guest limit is the host's soft limit. VLC (PPSA85300) imports it for a `closefrom` loop in the child of its spawn helper ([libc_stubs.c](https://github.com/ZiZc3/VLC/blob/d46be6a68f4244466b041c2dc1b640915ac1a983/ps5/compat/libc_stubs.c#L66-L72)), which never runs because its `fork` fails with `ENOSYS` - [close, sceKernelClose](../../../core/libs/prx/libkernel/File/src/HostStreams.cpp) (libkernel) - descriptors 0 to 2 that are open when libkernel loads belong to the host process: closing one fails with `EPERM` (`sceKernelClose`: `0x80020001`) and leaves it open, as closing the open descriptors 0 to 2 of a payload process did on a PS5 (firmware 13.60); whether a title process can close them is unverified. PPSA21703 calls `sceKernelClose(0)` repeatedly; closing the host stream let the host C runtime give descriptor 0 to the next file opened, which the next call then closed - [sceKernelPrintBacktraceWithModuleInfo](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws @@ -216,7 +215,7 @@ - [msync](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `kern_msync` and `vm_map_sync`, not measured on the console: the address is rounded down to the page, a range that wraps is `EINVAL`, as is `MS_ASYNC` with `MS_INVALIDATE`, and a range whose start or any page is not mapped (as `sceKernelVirtualQuery` reports it, reservations included) is `ENOMEM`; a page unmapped from inside a larger mapping still counts as mapped there, since `sceKernelVirtualQuery` keeps reporting the whole mapping. The guest has no file mappings, so a successful call writes nothing back. `MS_INVALIDATE` on a mapped range throws: FreeBSD returns `EBUSY` when the range holds `mlock`ed pages, and locked pages are not tracked - [madvise](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) accepts `MADV_PROTECT` (10) as a no-op hint without modifying process protection. FreeBSD 11's `kern_madvise` (`sys/vm/vm_mmap.c`) handles it before the address check, ignores the address and length, and calls `kern_procctl(..., PROC_SPROTECT, PPROT_SET)`, which requires `PRIV_VM_MADV_PROTECT` and fails with `EPERM` without it. Returning 0 unconditionally assumes the calling process is privileged. Console behaviour is unverified - [mincore](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `sys_mincore` (`sys/vm/vm_mmap.c`), not measured on the console: the address is rounded down to the page, length 0 returns 0, a null vector over a mapped range returns `EFAULT`, an overflowing range or any unmapped page in the range (as reported by `sceKernelVirtualQuery`) returns `ENOMEM`. For valid ranges, a page reports `MINCORE_INCORE` (0x1) when `sceKernelVirtualQuery` reports it committed and 0 when it is only reserved; actual residency, referenced and modified bits are not modelled. Length 0 returns 0 when the start page is mapped and `ENOMEM` otherwise, and the range and mapping checks come before the null vector `EFAULT`, as in `kern_mincore` Java 25 LAN / DNS (PPSA99869) imports it -- [mlockall, munlockall](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `mlockall` returns `EINVAL` for zero or unknown flags. Valid `MCL_CURRENT` throws because it would lock host stacks, heaps, modules and driver mappings along with guest memory; `MCL_FUTURE` throws because later mappings are not tracked for locking. FreeBSD 11.4's `sys_munlockall` (`sys/vm/vm_mmap.c`:1133-1165) checks `priv_check(td, PRIV_VM_MUNLOCK)` first (returning `EPERM` without it) and unwires the user map (`vm_map_unwire` over `vm_map_min`...`vm_map_max` with `VM_MAP_WIRE_HOLESOK`). Returning 0 unconditionally assumes the calling process is privileged. `munlockall` unlocks memory locked by `mlock` (`VirtualUnlock` on Windows, tolerating `ERROR_NOT_LOCKED` for pages never locked, and `munlockall` on Linux). On main, XPSemu (PPSA97358) fails to load at these imports (mlockall #4386, munlockall); their calls in QEMU `os-posix.c` execute only when `-overcommit mem-lock=on` is enabled. The call has not been observed in a run with this implementation. +- [mlockall, munlockall](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `mlockall` returns `EINVAL` for zero or unknown flags. Valid `MCL_CURRENT` throws because it would lock host stacks, heaps, modules and driver mappings along with guest memory; `MCL_FUTURE` throws because later mappings are not tracked for locking. FreeBSD 11.4's `sys_munlockall` (`sys/vm/vm_mmap.c`:1133-1165) checks `priv_check(td, PRIV_VM_MUNLOCK)` first (returning `EPERM` without it) and unwires the user map (`vm_map_unwire` over `vm_map_min`...`vm_map_max` with `VM_MAP_WIRE_HOLESOK`). Returning 0 unconditionally assumes the calling process is privileged. `munlockall` unlocks memory locked by `mlock` (`VirtualUnlock` on Windows, tolerating `ERROR_NOT_LOCKED` for pages never locked, and `munlockall` on Linux). XPSemu (PPSA97358) imports `mlockall` and `munlockall`; their calls in QEMU `os-posix.c` execute only when `-overcommit mem-lock=on` is enabled. The call has not been observed in a run with this implementation. - [socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) creates only `SOCK_STREAM` and `SOCK_DGRAM` sockets in `AF_INET` and `AF_INET6`. A type that FreeBSD 11.4's inet domain has no protocol for in any configuration ([`in_proto.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/netinet/in_proto.c) has no `SOCK_RDM` entry and no unknown type) fails with `EPROTOTYPE` when the protocol is 0 and `EPROTONOSUPPORT` otherwise, as [`socreate`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c) does. `SOCK_RAW`, which `in_proto.c` always has (`socreate` creates it for a privileged process), and `SOCK_SEQPACKET`, which it has only in a kernel built with `SCTP`, fail with `EPROTONOSUPPORT`; whether the console allows either is not known. `SOCK_DGRAM` with `IPPROTO_UDPLITE` and `AF_UNIX` (`socketpair` supports it) fail with `EPROTONOSUPPORT` and `EAFNOSUPPORT` where FreeBSD creates the socket - [sigtimedwait](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) waits on a process-wide pending set instead of throwing: a signal raised while blocked stays pending and is taken by `sigwait`, `sigwaitinfo` or `sigtimedwait`, delivered to the handler when `_sigprocmask` unblocks it, or delivered by `pause`. The set covers all 128 signals and the lowest number that is pending and in the set is selected (FreeBSD specifies the lowest number only among real-time signals). Selection, `EAGAIN` on timeout, `EINVAL` for a `tv_nsec` outside 0 to 1000000000 while nothing is pending and `NULL` timeout blocking indefinitely follow FreeBSD's [sigtimedwait(2)](https://man.freebsd.org/cgi/man.cgi?query=sigtimedwait). `sigwait` returns 0 with the number in its output, as POSIX specifies, and `sigwaitinfo` and `sigtimedwait` return the number, as FreeBSD does. The guest `siginfo_t` follows FreeBSD 11's `sys/sys/siginfo.h`: 80 bytes, `si_signo` at 0, `si_code` at 8, `si_pid` at 12, `si_uid` at 16, `si_addr` at 24, `si_value` at 32 and `si_band` at 40; only `si_signo`, `si_code` (`SI_USER`) and `si_pid` (the guest pid) are written, `si_uid` stays 0 because no uid is modelled, and the console's layout is not verified. PPSA85300 imports `sigtimedwait` and fails to load without it; its only call site is VLC's `vlc_writev` ([filesystem.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/posix/filesystem.c#L243-L278)), which polls with a zero timeout to drain a `SIGPIPE` only after `writev` fails with `EPIPE`. The waits are not cancellation points and an unblocked caught signal does not interrupt them with `EINTR`, where FreeBSD's `sigwaitinfo` and `sigtimedwait` may return it. `SIGPIPE` has no host equivalent and is never raised by a host write here (see `read, write and vector I/O`), so VLC's drain finds nothing - [pause, sigsuspend, sigwait, sigwaitinfo](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) take the pending set of `sigtimedwait` instead of throwing. `pause` waits for a pending signal that is not blocked and whose disposition can be delivered, runs its handler on the calling thread and returns -1 with `EINTR`; an ignored signal is discarded and the wait continues, and it never returns for a signal whose handler cannot run. A signal whose registered handler has `SA_SIGINFO` still throws, and one whose disposition is the default action and that has no host equivalent (every signal except 2, 4, 6, 8, 11 and 15) is neither delivered nor discarded: it stays pending until a wait takes it, where FreeBSD runs the default action, which terminates the process for most of them. A pending `SIGALRM` with the default disposition exits the process with status 142 once it is unblocked, as FreeBSD's default action does. PPSA68091, PPSA99169, PPSA99420 and PPSA97358 import them and fail to load without them. PPSA68091 calls `pause` at one site (0x8F7266), in a loop that waits while a global holds another thread's `errno` address. PPSA99169 only stores the address of `pause` in a table of symbol addresses (0x30DB1). PPSA99420 calls `pause` at 0x3BD80F6 and 0x3C9E220, `sigwait` at 0x3C9E4C7 and `sigwaitinfo` at 0x3C9E588. PPSA97358 calls `sigwait` from QEMU's `sigwait_compat` thread ([compatfd.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/compatfd.c#L28-L53)), which `qemu_signal_init` ([main-loop.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/main-loop.c#L110-L147)) starts for `SIGIO`, `SIGALRM` and `SIGBUS` when the main loop is created, so it reaches the wait at start-up, and from the vCPU thread of the dummy accelerator ([dummy-cpus.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/accel/dummy-cpus.c#L48-L56)). `sigsuspend` swaps the process-wide mask of `_sigprocmask` for its argument, waits as `pause` does through the same helper, then restores the mask through `_sigprocmask`, and returns -1 with `EINTR`; a null set throws, as the console faults in libthr instead of returning `EFAULT`. All four words of the argument replace the mask and are restored afterwards, as in `_sigprocmask`. Java 25 LAN / DNS (PPSA99869, #5501) imports it and fails to load without it, after `sysctlnametomib`, `fegetenv` and `mincore`. None of the five waits is a cancellation point @@ -245,7 +244,7 @@ - [pthread_cond_reltimedwait_np](../../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) takes `(cond, mutex, usec)`, a relative timeout in microseconds, and is `scePthreadCondTimedwait` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadCondTimedwait` ([`condvar.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/condvar.cpp#L229-L234)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `sem_reltimedwait_np`. Unverified on the console - [pthread_mutex_reltimedlock_np](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) takes `(mutex, usec)`, a relative timeout in microseconds, and is `scePthreadMutexTimedlock` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadMutexTimedlock` ([`mutex.cpp`](https://github.com/shadps4-emu/shadPS4/blob/7c960eae136b956cf4c1d72aacd2426b650330df/src/core/libraries/kernel/threads/mutex.cpp#L284-L287)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `pthread_cond_reltimedwait_np`, so a count of 2^32 microseconds or more would be truncated if the console reads 64 bits. Unverified on the console - [sysctl kern.arandom](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) serves FreeBSD 11's MIB {1, 37} (CTL_KERN, KERN_ARANDOM) and the "kern.arandom" sysctlbyname lookup through std::random_device, returning min(*oldLength, 256) random bytes. Following FreeBSD's sysctl_kern_arnd, a null buffer reports min(*oldLength, 256) without writing, a null length returns 0 without writing, and requests with a new value return EPERM (1) as the node is read-only. Cryptographic quality depends on the host's random device implementation. -- [pthread_atfork](../../../core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp) (libkernel) appends the three handlers to a process-wide list and returns 0, or `ENOMEM` when the entry cannot be allocated, as FreeBSD 11.4's [`_pthread_atfork`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_fork.c#L78-L100) does; null handlers are accepted. The handlers never run: `fork` throws and `vfork` is absent, so no child process is created. Handlers of a module that is unloaded stay in the list, where FreeBSD's `__pthread_cxa_finalize` removes them. PPSA97358 has a source-level call from the constructor `rcu_init` ([rcu.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/rcu.c#L488-L495)), but current-main runtime reachability is unverified; XPSemu currently stops first at the unresolved `mlockall` import (#4386) +- [pthread_atfork](../../../core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp) (libkernel) appends the three handlers to a process-wide list and returns 0, or `ENOMEM` when the entry cannot be allocated, as FreeBSD 11.4's [`_pthread_atfork`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_fork.c#L78-L100) does; null handlers are accepted. The handlers never run: `fork` throws and `vfork` is absent, so no child process is created. Handlers of a module that is unloaded stay in the list, where FreeBSD's `__pthread_cxa_finalize` removes them. PPSA97358 has a source-level call from the constructor `rcu_init` ([rcu.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/rcu.c#L488-L495)), but current-main runtime reachability is unverified - [sceKernelGetPageTableStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) reports 8192 entries on each side and counts them as a PS5 (firmware 13.60) counted them for a payload process: one cpu entry for each 2 MiB granule that holds a committed mapping, whatever its protection (none included), and one gpu entry for each 1 GiB region that holds a GPU-accessible mapping, plus one for each 2 MiB granule of such a mapping that is not a whole granule of direct memory at a 2 MiB-aligned physical address. A granule or region shared by several mappings counts once, and reservations do not count. On the console the first GPU mapping of a process also takes three gpu entries and one cpu entry that stay after it is unmapped, which is not modelled, and only the ranges whose protection is recorded here are counted. A null pointer returns `SCE_KERNEL_ERROR_EFAULT`, where the console faults. Resonance: A Plague Tale Legacy (PPSA30601) calls it at start-up (#4067) - [clock_settime](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) validates like FreeBSD 11.4's [`kern_clock_settime`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_time.c#L396-L412) for a root caller: `EFAULT` for a null time, `EINVAL` for any clock but `CLOCK_REALTIME`, a negative second count or a nanosecond count outside 0 to 999999999. A valid `CLOCK_REALTIME` request throws, naming the requested time, since the host clock is never set and no guest clock offset is kept. FreeBSD checks `PRIV_CLOCK_SETTIME` first, which a jailed process fails with `EPERM`; whether a title holds it on the console is unverified. PS5MC (PPSA99420) imports it; no call site is known - [Short sleeps ended by GPU and flip completions](../../../core/libs/prx/libkernel/Time/SleepWake.cpp) (libkernel) - a `sceKernelUsleep`, `usleep`, `nanosleep` or `sceKernelNanosleep` of 1 ms or less returns before the requested time, which FreeBSD and the console do not do: it ends once the AGC driver reports a completed GPU batch or batch completion action, or VideoOut completes a flip, so a title that polls a GPU label or the flip status between sleeps does not sleep the rest. A title that paces frames with such a sleep can wake up to 1 ms early. A thread returns early at most 8 times in a run of sleeps; the run restarts when the thread submits AGC work or did not sleep the requested time since its last sleep, so a loop that counts sleeps to time out still sleeps all but 8 in full. `APS5_NO_SLEEP_WAKE=1` keeps every sleep in full From 2aaf519eb78d0e36b6379720971f4de6d147ed13 Mon Sep 17 00:00:00 2001 From: orezraey Date: Fri, 23 Oct 2026 13:30:58 -0300 Subject: [PATCH 140/428] fix(libkernel): check socket address families and lengths as measured on a PS5 Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 27 ++++++++++--------- core/libs/tests/GuestSocketAddressLength.cpp | 28 ++++++++++++++++++++ core/libs/tests/GuestSocketPortZero.cpp | 8 ++++++ docs/dev/debt/libkernel.md | 6 ++--- 4 files changed, 54 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index f17394f4e4..94976715e3 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -272,11 +272,12 @@ int AddressLengthError(const void* input, std::uint32_t length) { if (length < 2) return 22; return input ? 0 : 14; } -bool Address(const void* input, std::uint32_t length, sockaddr_storage& native, socklen_t& size) { +bool Address(int family, const void* input, std::uint32_t length, sockaddr_storage& native, socklen_t& size) { if (const int error = AddressLengthError(input, length)) { Fail(error); return false; } const auto* bytes = static_cast(input); - if ((bytes[1] == 2 && length < 16) || (bytes[1] == 28 && length < 28)) { Fail(22); return false; } - if (bytes[1] == 2 && length >= 16) { + if (family == 2 && bytes[1] != 2) { Fail(47); return false; } + if ((family == 2 && length != 16) || (family == 28 && length != 28)) { Fail(22); return false; } + if (family == 2) { auto& v4 = reinterpret_cast(native); v4.sin_family = AF_INET; std::memcpy(&v4.sin_port, bytes + 2, 2); @@ -284,7 +285,7 @@ bool Address(const void* input, std::uint32_t length, sockaddr_storage& native, size = sizeof(v4); return true; } - if (bytes[1] == 28 && length >= 28) { + if (family == 28 && bytes[1] == 28) { auto& v6 = reinterpret_cast(native); v6.sin6_family = AF_INET6; std::memcpy(&v6.sin6_port, bytes + 2, 2); @@ -304,13 +305,17 @@ bool Listening(const Socket& socket) { bool listening = false; return GuestSockets::NativeListening(static_cast(socket.value), listening) && listening; } +int Disconnect(const Socket& socket); bool ZeroPortConnect(const Socket& socket, const void* address, std::uint32_t length) { if (!address || (socket.type != 1 && socket.type != 2) || (socket.family != 2 && socket.family != 28)) return false; const auto* bytes = static_cast(address); if (length < (socket.family == 2 ? 16u : 28u) || bytes[1] != socket.family || (bytes[2] | bytes[3]) != 0) return false; sockaddr_storage peer{}; socklen_t peerSize = sizeof(peer); - if (::getpeername(socket.value, reinterpret_cast(&peer), &peerSize) == 0) return false; + if (::getpeername(socket.value, reinterpret_cast(&peer), &peerSize) == 0) { + if (socket.type == 1) return false; + if (Disconnect(socket) != 0) return true; + } Fail(49); return true; } @@ -1146,7 +1151,7 @@ int APS5_VABI socket_nid_postfix(int family, int type, int protocol) { if (family != 2 && family != 28) return Fail(47); const int nativeType = plainType == 1 ? SOCK_STREAM : plainType == 2 ? SOCK_DGRAM : -1; const int expectedProtocol = plainType == 1 ? 6 : 17; - const bool inetKnowsType = plainType == 3 || plainType == 5; + const bool inetKnowsType = plainType == 3; if (nativeType == -1) return Fail(protocol == 0 && plainType != 0 && !inetKnowsType ? 41 : 43); if (protocol != 0 && protocol != expectedProtocol) return Fail(43); #ifdef _WIN32 @@ -1228,15 +1233,14 @@ int APS5_VABI connect_nid_postfix(int descriptor, const void* address, std::uint if (socket->family != GuestUnix && socket->type == 2 && address && length >= 2 && static_cast(address)[1] == 0 && Disconnect(*socket) != 0) return -1; + if (socket->type == 1 && Listening(*socket)) return Fail(45); sockaddr_storage native{}; socklen_t size; - if (!Address(address, length, native, size)) return -1; + if (!Address(socket->family, address, length, native, size)) return -1; #ifdef _WIN32 if (socket->type == 1) RefuseClosedLoopbackAtOnce_nid_no_patch(socket->value, reinterpret_cast(&native)); #endif - if (socket->type == 1 && Listening(*socket)) return Fail(45); if (ZeroPortConnect(*socket, address, length)) return -1; - if (native.ss_family != (socket->family == 2 ? AF_INET : AF_INET6)) return Fail(47); { std::lock_guard lock(socket->modeMutex); socket->options.reportedError = 0; @@ -1314,7 +1318,7 @@ int APS5_VABI bind_nid_postfix(int descriptor, const void* address, std::uint32_ if (!socket) return -1; sockaddr_storage native{}; socklen_t size; - if (!Address(address, length, native, size)) return -1; + if (!Address(socket->family, address, length, native, size)) return -1; return ::bind(socket->value, reinterpret_cast(&native), size) ? Fail(NativeError()) : 0; } int APS5_VABI shutdown_nid_postfix(int descriptor, int how) { @@ -1400,8 +1404,7 @@ std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, st } else { sockaddr_storage native{}; socklen_t size; - if (!Address(address, addressLength, native, size)) return -1; - if (native.ss_family != (socket->family == 2 ? AF_INET : AF_INET6)) return Fail(47); + if (!Address(socket->family, address, addressLength, native, size)) return -1; if (const auto answered = ZeroPortSend(*socket, native, length); answered != NotAnswered) return answered; result = static_cast(::sendto(socket->value, static_cast(buffer), static_cast(length), SocketSendFlags(*socket, flags), reinterpret_cast(&native), size)); diff --git a/core/libs/tests/GuestSocketAddressLength.cpp b/core/libs/tests/GuestSocketAddressLength.cpp index d18dd5910c..f2bcb1be56 100644 --- a/core/libs/tests/GuestSocketAddressLength.cpp +++ b/core/libs/tests/GuestSocketAddressLength.cpp @@ -69,6 +69,33 @@ void ArgumentsAreCheckedBeforeTheDescriptor() { Require(Fails(sendto_nid_postfix(-1, "x", 1, 0, address.data(), address.size()), ErrorBadDescriptor), "sendto(-1) with a valid address must fail with EBADF (9)"); } +void LongAddressesAndOtherFamiliesFail() { + constexpr int ErrorFamily = 47; + const int stream = socket_nid_postfix(2, Stream, 0); + const int datagram = socket_nid_postfix(2, Datagram, 0); + const int stream6 = socket_nid_postfix(28, Stream, 0); + Require(stream >= 0 && datagram >= 0 && stream6 >= 0, "socket failed"); + std::array address{16, 2}; + address[4] = 127; + address[7] = 1; + for (const std::uint32_t length : {17u, 28u, 255u}) { + Require(Fails(bind_nid_postfix(datagram, address.data(), length), ErrorInvalid), "bind with an AF_INET address longer than sockaddr_in must fail with EINVAL (22)"); + Require(Fails(connect_nid_postfix(datagram, address.data(), length), ErrorInvalid), "connect with an AF_INET address longer than sockaddr_in must fail with EINVAL (22)"); + Require(Fails(sendto_nid_postfix(datagram, "x", 1, 0, address.data(), length), ErrorInvalid), "sendto with an AF_INET address longer than sockaddr_in must fail with EINVAL (22)"); + } + std::array v6{28, 28}; + v6[23] = 1; + Require(Fails(bind_nid_postfix(stream6, v6.data(), 29), ErrorInvalid), "bind with an AF_INET6 address longer than sockaddr_in6 must fail with EINVAL (22)"); + Require(Fails(connect_nid_postfix(stream6, v6.data(), 64), ErrorInvalid), "connect with an AF_INET6 address longer than sockaddr_in6 must fail with EINVAL (22)"); + for (const std::uint8_t family : {std::uint8_t{28}, std::uint8_t{1}, std::uint8_t{0}}) { + address[1] = family; + Require(Fails(bind_nid_postfix(stream, address.data(), 16), ErrorFamily), "bind of another family on an AF_INET socket must fail with EAFNOSUPPORT (47)"); + } + address[1] = 28; + Require(Fails(bind_nid_postfix(stream, address.data(), 28), ErrorFamily), "the family is checked before the length on an AF_INET socket"); + Require(close_nid_postfix(stream) == 0 && close_nid_postfix(datagram) == 0 && close_nid_postfix(stream6) == 0, "close failed"); +} + void FullAddressesStillWork() { const int stream = socket_nid_postfix(2, Stream, 0); Require(stream >= 0, "socket failed"); @@ -82,6 +109,7 @@ void FullAddressesStillWork() { int main() { ShortAddressesAreInvalid(); ArgumentsAreCheckedBeforeTheDescriptor(); + LongAddressesAndOtherFamiliesFail(); FullAddressesStillWork(); return 0; } diff --git a/core/libs/tests/GuestSocketPortZero.cpp b/core/libs/tests/GuestSocketPortZero.cpp index 070b9c8982..b0fca2cc7b 100644 --- a/core/libs/tests/GuestSocketPortZero.cpp +++ b/core/libs/tests/GuestSocketPortZero.cpp @@ -45,6 +45,14 @@ static void CheckDatagram(int family) { Require(connect_nid_postfix(probe, address.data(), size) == -1 && *__error_nid_postfix() == 49, "connect to port 0 on an unconnected datagram socket must fail with EADDRNOTAVAIL (49)"); Require(getpeername_nid_postfix(probe, name.data(), &length) == -1 && *__error_nid_postfix() == 57, "still unconnected"); + address[3] = 9; + Require(connect_nid_postfix(probe, address.data(), size) == 0, "connect to port 9"); + address[3] = 0; + Require(connect_nid_postfix(probe, address.data(), size) == -1 && *__error_nid_postfix() == 49, + "connect to port 0 on a connected datagram socket must fail with EADDRNOTAVAIL (49)"); + length = name.size(); + Require(getpeername_nid_postfix(probe, name.data(), &length) == -1 && *__error_nid_postfix() == 57, + "connect to port 0 must disconnect a datagram socket first"); Require(close_nid_postfix(probe) == 0, "close"); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 515c8d8c2b..f1214a5fc5 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -200,7 +200,7 @@ - [MSG_WAITALL](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) in `recv`, `recvfrom` and `recvmsg` on a stream socket is passed to the host, which returns when the buffer is full, the peer closes or an error occurs, as FreeBSD 11.4's [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1855-L1860) does; a datagram socket ignores it. It is not measured on a console, a nonblocking socket and a call that also passes `MSG_PEEK` follow the host's rules, and `SO_RCVTIMEO` bounds only the wait for the first data. Any other flag still fails with `EOPNOTSUPP`. - [recv and recvfrom](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answer without the host a zero-length receive on a stream socket (0, or `ENOTCONN` when it is not connected, [L1566-L1578](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1566-L1578)) and, on Windows only, a receive on a socket whose read side `shutdown` closed (0, as [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1556-L1562) returns at `SBS_CANTRCVMORE`); Winsock fails the second with `EPIPE` and blocks on the first. Linux and macOS still return data buffered before the `shutdown`, where FreeBSD discards it. `recvmsg` still asks the host in both cases. Not measured on a console. - [send, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - send and sendto without an address on an unconnected datagram socket map Windows `WSAENOTCONN` to FreeBSD's `EDESTADDRREQ` (39). -- [bind, connect, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) check a socket address length as FreeBSD 11.4's [`getsockaddr`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_syscalls.c) and the `sa_len` test of `tcp_usr_connect` and `in_pcbconnect` do: below 2 or an `AF_INET` address below 16 or an `AF_INET6` address below 28 bytes fails with `EINVAL`, above 255 with `ENAMETOOLONG`. An address longer than `sockaddr_in` or `sockaddr_in6` is still accepted where FreeBSD answers `EINVAL`, and an address of another family still fails with `EAFNOSUPPORT`, as the console's behaviour for these is not known. None of this was measured on a console. +- [bind, connect, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) check a socket address as `bind` and `connect` did in a payload process on a PS5 (firmware 13.60): a length below 2 fails with `EINVAL` and above 255 with `ENAMETOOLONG`; on an `AF_INET` socket another family fails with `EAFNOSUPPORT` and an `AF_INET` address of any length but 16 with `EINVAL`; on an `AF_INET6` socket any length but 28 fails with `EINVAL`. `sendto`, and another family on an `AF_INET6` socket (`EAFNOSUPPORT`, after the length as FreeBSD 11.4's `in6_pcbbind` checks it), were not measured. `connect` on a listening stream socket fails with `EOPNOTSUPP` before the family and length checks, as FreeBSD 11.4's `soconnect` does; that order was not measured either - [read, write and vector I/O](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. `EPIPE` from a write raises the host's `SIGPIPE` on Linux unless the socket has `SO_NOSIGPIPE` set, and the guest cannot ignore it: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; nothing is raised on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` - [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number @@ -212,12 +212,12 @@ - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level, `TCP_NODELAY` and `IPV6_V6ONLY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal. An accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. `SO_SNDTIMEO` and `SO_RCVTIMEO` follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689) and [`sogetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2846-L2850). A 16-byte `timeval` with a negative field or 1000000 microseconds or more fails with `EDOM`, and more than `INT32_MAX` seconds becomes the longest wait. The value is kept and read back through FreeBSD's 32.32 fixed-point conversion ([`tvtosbt`, `sbttotv`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/time.h#L178-L286)), so half a second reads back as 499999 microseconds and one microsecond as 0. A `recv`, `recvfrom`, `recvmsg`, `read` or `readv`, and a `send`, `sendto`, `sendmsg`, `write` or `writev`, on a blocking socket then waits that long for the host to report the socket readable or writable, and fails with `EWOULDBLOCK`; a call with `MSG_DONTWAIT` does not wait. An accepted socket takes both from its listener ([`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L629-L630)). The wait covers only the first transfer: once the socket is writable the host send runs without a limit, where FreeBSD stops a send that cannot finish and returns the bytes sent so far, and a receive whose data another thread takes between the wait and the host call blocks again. `sendfile` ignores the send timeout. The host wait has millisecond steps, so a shorter timeout lasts up to a millisecond longer. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. `IPV6_V6ONLY` (`IPPROTO_IPV6`, 27) on an `AF_INET6` socket is set on the host socket and reads back as 0 or 1, after FreeBSD's [`ip6_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1716-L1732): a value that is not exactly an `int` fails with `EINVAL` ([L1572](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1572)), as does setting it on a bound socket, which the host refuses too. `socket` turns it on for every `AF_INET6` socket, as FreeBSD's default `net.inet6.ip6.v6only` does, and Java turns it off on the sockets it opens: Java 25 Advanced Network (PPSA99871) stops at `java.net.SocketException: Protocol not available` without it. The `IPPROTO_IPV6` level on an `AF_INET` or `AF_UNIX` socket fails with `EINVAL`, as `ip_ctloutput` and `uipc_ctloutput` answer it. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores, the low-water marks among them; PPSA99871 sets `SO_REUSEPORT` and carries on when it fails), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them), every other `IPPROTO_IPV6` option ([L1928-L1929](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1928-L1929), [L2158-L2159](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L2158-L2159)) and the `IPPROTO_IP` level fail with `ENOPROTOOPT`. None of this was measured on a console. - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had -- [connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - `connect` to port 0 on an unconnected `AF_INET` or `AF_INET6` stream or datagram socket fails with `EADDRNOTAVAIL` (49), as FreeBSD 11.4's `in_pcbconnect_setup()` and `in6_pcbconnect()` do. A connected stream socket keeps its host `EISCONN` answer. Known gaps: on an already connected datagram socket `connect` to port 0 keeps the old peer, where FreeBSD's `soconnect()` disconnects first and then fails with 49; `sendto` and `sendmsg` to port 0 on an `AF_INET` datagram socket fail with 49 unless the socket is bound to a specific address and port and the destination is neither `INADDR_ANY` nor `INADDR_BROADCAST`, which is the path where `udp_output()` skips `in_pcbconnect_setup()` and FreeBSD sends to port 0. There nothing can receive on port 0, so the call returns the length without the host, because Linux's `udp_sendmsg` refuses port 0 with `EINVAL`; an `AF_INET6` socket still goes to the host. The answers are taken from the FreeBSD source and are not measured on a console. [sceNetConnect](../../../core/libs/prx/libSceNet/Export.cpp) is not changed +- [connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - `connect` to port 0 on an `AF_INET` or `AF_INET6` stream or datagram socket fails with `EADDRNOTAVAIL` (49), and on a connected datagram socket it disconnects first, as on a PS5 (firmware 13.60) and in FreeBSD 11.4's `soconnect()`. A connected stream socket keeps its host `EISCONN` answer. Known gap: `sendto` and `sendmsg` to port 0 on an `AF_INET` datagram socket fail with 49 (measured for an unbound socket) unless the socket is bound to a specific address and port and the destination is neither `INADDR_ANY` nor `INADDR_BROADCAST`, which is the path where `udp_output()` skips `in_pcbconnect_setup()` and FreeBSD sends to port 0. There the call returns the length without the host, as Linux's `udp_sendmsg` refuses port 0 with `EINVAL`; an `AF_INET6` socket still goes to the host. [sceNetConnect](../../../core/libs/prx/libSceNet/Export.cpp) is not changed - [msync](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `kern_msync` and `vm_map_sync`, not measured on the console: the address is rounded down to the page, a range that wraps is `EINVAL`, as is `MS_ASYNC` with `MS_INVALIDATE`, and a range whose start or any page is not mapped (as `sceKernelVirtualQuery` reports it, reservations included) is `ENOMEM`; a page unmapped from inside a larger mapping still counts as mapped there, since `sceKernelVirtualQuery` keeps reporting the whole mapping. The guest has no file mappings, so a successful call writes nothing back. `MS_INVALIDATE` on a mapped range throws: FreeBSD returns `EBUSY` when the range holds `mlock`ed pages, and locked pages are not tracked - [madvise](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) accepts `MADV_PROTECT` (10) as a no-op hint without modifying process protection. FreeBSD 11's `kern_madvise` (`sys/vm/vm_mmap.c`) handles it before the address check, ignores the address and length, and calls `kern_procctl(..., PROC_SPROTECT, PPROT_SET)`, which requires `PRIV_VM_MADV_PROTECT` and fails with `EPERM` without it. Returning 0 unconditionally assumes the calling process is privileged. Console behaviour is unverified - [mincore](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `sys_mincore` (`sys/vm/vm_mmap.c`), not measured on the console: the address is rounded down to the page, length 0 returns 0, a null vector over a mapped range returns `EFAULT`, an overflowing range or any unmapped page in the range (as reported by `sceKernelVirtualQuery`) returns `ENOMEM`. For valid ranges, a page reports `MINCORE_INCORE` (0x1) when `sceKernelVirtualQuery` reports it committed and 0 when it is only reserved; actual residency, referenced and modified bits are not modelled. Length 0 returns 0 when the start page is mapped and `ENOMEM` otherwise, and the range and mapping checks come before the null vector `EFAULT`, as in `kern_mincore` Java 25 LAN / DNS (PPSA99869) imports it - [mlockall, munlockall](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `mlockall` returns `EINVAL` for zero or unknown flags. Valid `MCL_CURRENT` throws because it would lock host stacks, heaps, modules and driver mappings along with guest memory; `MCL_FUTURE` throws because later mappings are not tracked for locking. FreeBSD 11.4's `sys_munlockall` (`sys/vm/vm_mmap.c`:1133-1165) checks `priv_check(td, PRIV_VM_MUNLOCK)` first (returning `EPERM` without it) and unwires the user map (`vm_map_unwire` over `vm_map_min`...`vm_map_max` with `VM_MAP_WIRE_HOLESOK`). Returning 0 unconditionally assumes the calling process is privileged. `munlockall` unlocks memory locked by `mlock` (`VirtualUnlock` on Windows, tolerating `ERROR_NOT_LOCKED` for pages never locked, and `munlockall` on Linux). On main, XPSemu (PPSA97358) fails to load at these imports (mlockall #4386, munlockall); their calls in QEMU `os-posix.c` execute only when `-overcommit mem-lock=on` is enabled. The call has not been observed in a run with this implementation. -- [socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) creates only `SOCK_STREAM` and `SOCK_DGRAM` sockets in `AF_INET` and `AF_INET6`. A type that FreeBSD 11.4's inet domain has no protocol for in any configuration ([`in_proto.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/netinet/in_proto.c) has no `SOCK_RDM` entry and no unknown type) fails with `EPROTOTYPE` when the protocol is 0 and `EPROTONOSUPPORT` otherwise, as [`socreate`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c) does. `SOCK_RAW`, which `in_proto.c` always has (`socreate` creates it for a privileged process), and `SOCK_SEQPACKET`, which it has only in a kernel built with `SCTP`, fail with `EPROTONOSUPPORT`; whether the console allows either is not known. `SOCK_DGRAM` with `IPPROTO_UDPLITE` and `AF_UNIX` (`socketpair` supports it) fail with `EPROTONOSUPPORT` and `EAFNOSUPPORT` where FreeBSD creates the socket +- [socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) creates only `SOCK_STREAM` and `SOCK_DGRAM` sockets in `AF_INET` and `AF_INET6`. In a payload process on a PS5 (firmware 13.60) `SOCK_RDM`, `SOCK_SEQPACKET` and an unknown type failed with `EPROTOTYPE` when the protocol was 0 and `EPROTONOSUPPORT` otherwise, as here, and a family other than `AF_INET`, `AF_INET6` and `AF_UNIX` with `EAFNOSUPPORT`. The console also created `SOCK_RAW` (for that payload process, which may hold privileges a title does not), `SOCK_DGRAM` with `IPPROTO_UDPLITE`, and `AF_UNIX` stream, datagram and seqpacket sockets; these fail here with `EPROTONOSUPPORT` and `EAFNOSUPPORT` (`socketpair` supports `AF_UNIX`) - [sigtimedwait](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) waits on a process-wide pending set instead of throwing: a signal raised while blocked stays pending and is taken by `sigwait`, `sigwaitinfo` or `sigtimedwait`, delivered to the handler when `_sigprocmask` unblocks it, or delivered by `pause`. The set covers all 128 signals and the lowest number that is pending and in the set is selected (FreeBSD specifies the lowest number only among real-time signals). Selection, `EAGAIN` on timeout, `EINVAL` for a `tv_nsec` outside 0 to 1000000000 while nothing is pending and `NULL` timeout blocking indefinitely follow FreeBSD's [sigtimedwait(2)](https://man.freebsd.org/cgi/man.cgi?query=sigtimedwait). `sigwait` returns 0 with the number in its output, as POSIX specifies, and `sigwaitinfo` and `sigtimedwait` return the number, as FreeBSD does. The guest `siginfo_t` follows FreeBSD 11's `sys/sys/siginfo.h`: 80 bytes, `si_signo` at 0, `si_code` at 8, `si_pid` at 12, `si_uid` at 16, `si_addr` at 24, `si_value` at 32 and `si_band` at 40; only `si_signo`, `si_code` (`SI_USER`) and `si_pid` (the guest pid) are written, `si_uid` stays 0 because no uid is modelled, and the console's layout is not verified. PPSA85300 imports `sigtimedwait` and fails to load without it; its only call site is VLC's `vlc_writev` ([filesystem.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/posix/filesystem.c#L243-L278)), which polls with a zero timeout to drain a `SIGPIPE` only after `writev` fails with `EPIPE`. The waits are not cancellation points and an unblocked caught signal does not interrupt them with `EINTR`, where FreeBSD's `sigwaitinfo` and `sigtimedwait` may return it. `SIGPIPE` has no host equivalent and is never raised by a host write here (see `read, write and vector I/O`), so VLC's drain finds nothing - [pause, sigsuspend, sigwait, sigwaitinfo](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) take the pending set of `sigtimedwait` instead of throwing. `pause` waits for a pending signal that is not blocked and whose disposition can be delivered, runs its handler on the calling thread and returns -1 with `EINTR`; an ignored signal is discarded and the wait continues, and it never returns for a signal whose handler cannot run. A signal whose registered handler has `SA_SIGINFO` still throws, and one whose disposition is the default action and that has no host equivalent (every signal except 2, 4, 6, 8, 11 and 15) is neither delivered nor discarded: it stays pending until a wait takes it, where FreeBSD runs the default action, which terminates the process for most of them. A pending `SIGALRM` with the default disposition exits the process with status 142 once it is unblocked, as FreeBSD's default action does. PPSA68091, PPSA99169, PPSA99420 and PPSA97358 import them and fail to load without them. PPSA68091 calls `pause` at one site (0x8F7266), in a loop that waits while a global holds another thread's `errno` address. PPSA99169 only stores the address of `pause` in a table of symbol addresses (0x30DB1). PPSA99420 calls `pause` at 0x3BD80F6 and 0x3C9E220, `sigwait` at 0x3C9E4C7 and `sigwaitinfo` at 0x3C9E588. PPSA97358 calls `sigwait` from QEMU's `sigwait_compat` thread ([compatfd.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/compatfd.c#L28-L53)), which `qemu_signal_init` ([main-loop.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/main-loop.c#L110-L147)) starts for `SIGIO`, `SIGALRM` and `SIGBUS` when the main loop is created, so it reaches the wait at start-up, and from the vCPU thread of the dummy accelerator ([dummy-cpus.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/accel/dummy-cpus.c#L48-L56)). `sigsuspend` swaps the process-wide mask of `_sigprocmask` for its argument, waits as `pause` does through the same helper, then restores the mask through `_sigprocmask`, and returns -1 with `EINTR`; a null set throws, as the console faults in libthr instead of returning `EFAULT`. All four words of the argument replace the mask and are restored afterwards, as in `_sigprocmask`. Java 25 LAN / DNS (PPSA99869, #5501) imports it and fails to load without it, after `sysctlnametomib`, `fegetenv` and `mincore`. None of the five waits is a cancellation point - [getsid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns the calling process ID for pid 0 and the calling process, and `ESRCH` for other process IDs, assuming the single guest process is its own session leader. PPSA74126 imports it and fails to load without it From 304821c40ff7b9485a9d581a5cc7359d010284e4 Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Fri, 23 Oct 2026 11:34:02 -0500 Subject: [PATCH 141/428] test(libSceSaveData.native): cover null delete struct and directory name validation --- core/libs/tests/GuestSaveDataDelete.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/tests/GuestSaveDataDelete.cpp b/core/libs/tests/GuestSaveDataDelete.cpp index 0ebbce1462..b8eae7ef5c 100644 --- a/core/libs/tests/GuestSaveDataDelete.cpp +++ b/core/libs/tests/GuestSaveDataDelete.cpp @@ -130,6 +130,9 @@ int main() { Check(sceSaveDataUmount2(0, &mountResult.mount_point) == 0, "the mounted save unmounts"); } + Check(sceSaveDataDelete(nullptr) == SaveDataErrorParameter, "null delete struct is rejected"); + SaveDataDelete nullDir{}; + Check(sceSaveDataDelete(&nullDir) == SaveDataErrorParameter, "null dir_name pointer is rejected"); Check(Delete("kept", 5) == 0, "a valid name is deleted"); Check(!std::filesystem::exists(kept.parent_path()), "the valid save directory is gone"); Check(std::filesystem::exists(victim), "the directory beside the save root is untouched"); From 016ee49f30dc07ba432d833c4043c278ef5560a1 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Thu, 22 Oct 2026 02:08:40 +0700 Subject: [PATCH 142/428] docs(agc): name the 10_11_11 and 2_10_10_10 variants that FORCE_DEGAMMA leaves unchanged Co-Authored-By: Claude --- docs/dev/debt/libSceAgcDriver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index c680b6c7c3..a0fdd9b9c7 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -200,7 +200,7 @@ - [Packed 16-bit textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) - `5_6_5`, `1_5_5_5` and `4_4_4_4` are viewed as R5G6B5, A1R5G5B5 and B4G4R4A4 (the 4-bit format every Vulkan device can sample; R4G4B4A4 is optional and lavapipe lacks it) with a swizzle that reads X from the low bits, and a DST_SEL of the missing W of `5_6_5` reads X. Measured on RDNA2 (gfx1035), not on the console: through `image_load` and `image_sample_lz`, X, Y, Z (and W of `1_5_5_5` and `4_4_4_4`) come from the low, middle and high fields, and W of `5_6_5` returns X (the low 5 bits), not 1. Storage images use compatible R16_UINT views with the [buffer UNORM store conversion](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp); quantization uses nearest even and maps NaN to zero. Rounding ties and NaN handling for packed stores have not been checked on AMD or PS5 hardware. - [Min/max S# reduction](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FILTER_MODE` 1 and 2 use Vulkan's min and max reduction (the same `SQ_IMG_FILTER_MODE` values RADV programs). Measured on RDNA2 (gfx1035), not on the console: only texels of non-zero weight are reduced, and the weight is taken after rounding to the 8-bit subtexel grid (a sample within 1/512 texel of a centre reduces that texel alone); a driver with other `subTexelPrecisionBits` reduces a different set there. Gathers ignore the mode. - [Min/max S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver, shader recompiler) - throws with a linear mip filter, depth or color-texture compare, or bilinear or anisotropic filtering of an image the recompiler point-samples (sint, converted, depth-bits). With anisotropy the host sampler reduces over its own footprint; [RADV](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/amd/vulkan/radv_sampler.c#L185-L232) programs the same S# for such a Vulkan sampler. Measured on RDNA2 (gfx1030 in [#5156](https://github.com/boykopovar/AnyPS5/pull/5156#issuecomment-6155265616), gfx1035 in #5821), not on the console: without derivatives (`image_sample_lz`), min or max with anisotropic bilinear returns the bilinear minimum or maximum and with anisotropic point the point result, at every ratio field 0 to 4; with derivatives (`image_sample_d`, a 4:1 footprint), anisotropic bilinear at ratio field 2 or 4 reduces over the texels of all its anisotropic taps (the minimum or maximum along the major axis), not one bilinear footprint. A host that is not AMD reduces over its own anisotropic footprint, and Vulkan leaves nearest filtering with anisotropy to the host driver. -- [FORCE_DEGAMMA](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp) (libSceAgcDriver) - not measured on the console. A sampler that forces sRGB decoding throws for a format not measured on RDNA2. The rest follows gfx1035 and gfx1031 (point and bilinear): sRGB textures sample as without the bit, as in shadPS4 (PS4); UNORM `8`, `8_8`, `8_8_8_8`, BC1, BC2, BC3 and BC7 sample through their sRGB view (decoded before filtering), and SNORM `8`, `8_8` and `8_8_8_8` through the sRGB view of their bytes, as gfx1031 reads them; `16`, `16_16` and `16_16_16_16` UNORM, SNORM and FLOAT, `32`, `32_32` and `32_32_32_32` FLOAT, `10_11_11`, `2_10_10_10`, `5_9_9_9`, `5_6_5`, `1_5_5_5`, `4_4_4_4`, BC4, BC5 and BC6H sample as without the bit, as gfx1031 does. +- [FORCE_DEGAMMA](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp) (libSceAgcDriver) - not measured on the console. A sampler that forces sRGB decoding throws for a format not measured on RDNA2. The rest follows gfx1035 and gfx1031 (point and bilinear): sRGB textures sample as without the bit, as in shadPS4 (PS4); UNORM `8`, `8_8`, `8_8_8_8`, BC1, BC2, BC3 and BC7 sample through their sRGB view (decoded before filtering), and SNORM `8`, `8_8` and `8_8_8_8` through the sRGB view of their bytes, as gfx1031 reads them; `16`, `16_16` and `16_16_16_16` UNORM, SNORM and FLOAT, `32`, `32_32` and `32_32_32_32` FLOAT, `10_11_11` FLOAT, `2_10_10_10` UNORM, `5_9_9_9`, `5_6_5`, `1_5_5_5`, `4_4_4_4`, BC4, BC5 and BC6H sample as without the bit, as gfx1031 does. - [Tessellation factor ring](../../../core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp) (libSceAgcDriver) - `sceAgcDriverSetTFRing` records the ring and `sceAgcDriverGetTFRing` reports it. Before a title sets one, it reports the console's default, 0xFF0000000 and 128 KiB, as measured in [PS5_Vulkan's hardware findings](https://github.com/mihawk-99/PS5_Vulkan/blob/fde9e3798c2ec1d8fccc90d5067cd6060490607f/docs/HARDWARE_FINDINGS.md). Nothing is written to the recorded ring: tessellated draws run as Vulkan tessellation stages, which keep their factors on the host, so a title that reads tessellation factors from the ring finds none. A null base or a zero size throws; [ps5-opengl's AGC runtime](https://github.com/blackbearreloaded/ps5-opengl/blob/67c873f985eaefe0a72f713529e5632eaa01139c/src/platform/ps5_agc_native_runtime.c) calls `sceAgcDriverSetTFRing(0, 0)` in its cleanup, and what that does on the console is unknown. - [VertexBufferReadSize](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) and the [vertex staging copy](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - a vertex fetch whose index is at or past the descriptor's `num_records` no longer throws for `OOB_SELECT` 0 and 1. The copy still spans the whole fetch, but the bytes from `num_records * stride` onward read as zeros, so the out-of-range vertex is `(0, 0, 0, 0)`. Measured with `buffer_load_format_xyzw` (`idxen`) through the [hardware oracle](../HW_ORACLE.md) on RDNA2 gfx1036 for PPSA01325's word 3 `0x0004dfac` (`OOB_SELECT` 0) in #2241: indices 3, 4 and 5 over 3 records each return `00000000 00000000 00000000 00000000`, W included, unlike Vulkan `robustBufferAccess`, which returns `(0, 0, 0, 1)`. The zeros come from the record check, not from memory: stored values placed past the records are ignored, and a `num_records` 6 control returns them. `RESOURCE_LEVEL` 1, `OOB_SELECT` 1 and FORMAT 74 `32_32_32_FLOAT` give the same zeros, in wave32 and wave64; gfx1013 (the console) is not measured. `OOB_SELECT` 2 returns the stored values past the records and `OOB_SELECT` 3 compares `num_records` against the byte offset, both measured on the same setup in #2241; the two keep throwing past `num_records` here, since each needs its own rule. Zero-stride descriptors (`stride` 0) keep their separate byte-count check and are not covered here. Each out-of-range fetch is copied alone and outside the draw-snapshot cache, which is neither read nor written for it: a reused snapshot would carry another draw's bytes into the zero region, and a stored zero-padded copy would hand zeros to a later in-range draw at the same address. The valid prefix is still flushed for GPU writes before it is read. - [Draw snapshot buffer descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) rewrite each affected array from element zero, preserving unchanged descriptors. MoltenVK 1.4.2 writes auxiliary buffer lengths at index `i` instead of `start + i` for partial array updates ([MVKDescriptorSet.mm, BufferAuxSize](https://github.com/KhronosGroup/MoltenVK/blob/v1.4.2/MoltenVK/MoltenVK/GPUObjects/MVKDescriptorSet.mm#L1051)). Per-element writes therefore let one buffer length replace another even when Vulkan buffer handles and ranges are correct. From 1cf109240f126ab63276ee3a8858ec5537d781cb Mon Sep 17 00:00:00 2001 From: orezraey Date: Fri, 23 Oct 2026 15:24:31 -0300 Subject: [PATCH 143/428] fix(libkernel): check a sendto address length before its family as measured on a PS5 Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 8 +++++--- core/libs/tests/GuestSocketAddressLength.cpp | 5 +++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 94976715e3..9ac5a95298 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -272,11 +272,13 @@ int AddressLengthError(const void* input, std::uint32_t length) { if (length < 2) return 22; return input ? 0 : 14; } -bool Address(int family, const void* input, std::uint32_t length, sockaddr_storage& native, socklen_t& size) { +bool Address(int family, const void* input, std::uint32_t length, sockaddr_storage& native, socklen_t& size, bool lengthFirst = false) { if (const int error = AddressLengthError(input, length)) { Fail(error); return false; } const auto* bytes = static_cast(input); + const bool wrongLength = (family == 2 && length != 16) || (family == 28 && length != 28); + if (lengthFirst && wrongLength) { Fail(22); return false; } if (family == 2 && bytes[1] != 2) { Fail(47); return false; } - if ((family == 2 && length != 16) || (family == 28 && length != 28)) { Fail(22); return false; } + if (wrongLength) { Fail(22); return false; } if (family == 2) { auto& v4 = reinterpret_cast(native); v4.sin_family = AF_INET; @@ -1404,7 +1406,7 @@ std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, st } else { sockaddr_storage native{}; socklen_t size; - if (!Address(socket->family, address, addressLength, native, size)) return -1; + if (!Address(socket->family, address, addressLength, native, size, true)) return -1; if (const auto answered = ZeroPortSend(*socket, native, length); answered != NotAnswered) return answered; result = static_cast(::sendto(socket->value, static_cast(buffer), static_cast(length), SocketSendFlags(*socket, flags), reinterpret_cast(&native), size)); diff --git a/core/libs/tests/GuestSocketAddressLength.cpp b/core/libs/tests/GuestSocketAddressLength.cpp index f2bcb1be56..59b885d912 100644 --- a/core/libs/tests/GuestSocketAddressLength.cpp +++ b/core/libs/tests/GuestSocketAddressLength.cpp @@ -93,6 +93,11 @@ void LongAddressesAndOtherFamiliesFail() { } address[1] = 28; Require(Fails(bind_nid_postfix(stream, address.data(), 28), ErrorFamily), "the family is checked before the length on an AF_INET socket"); + Require(Fails(sendto_nid_postfix(datagram, "x", 1, 0, address.data(), 28), ErrorInvalid), "sendto checks the length before the family"); + Require(Fails(sendto_nid_postfix(datagram, "x", 1, 0, address.data(), 16), ErrorFamily), "sendto of another family fails with EAFNOSUPPORT (47)"); + address[1] = 2; + Require(Fails(bind_nid_postfix(stream6, address.data(), 16), ErrorInvalid), "an AF_INET6 socket checks the length before the family"); + Require(Fails(connect_nid_postfix(stream6, address.data(), 28), ErrorFamily), "an AF_INET address of the AF_INET6 length fails with EAFNOSUPPORT (47)"); Require(close_nid_postfix(stream) == 0 && close_nid_postfix(datagram) == 0 && close_nid_postfix(stream6) == 0, "close failed"); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index f1214a5fc5..c157a3a836 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -200,7 +200,7 @@ - [MSG_WAITALL](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) in `recv`, `recvfrom` and `recvmsg` on a stream socket is passed to the host, which returns when the buffer is full, the peer closes or an error occurs, as FreeBSD 11.4's [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1855-L1860) does; a datagram socket ignores it. It is not measured on a console, a nonblocking socket and a call that also passes `MSG_PEEK` follow the host's rules, and `SO_RCVTIMEO` bounds only the wait for the first data. Any other flag still fails with `EOPNOTSUPP`. - [recv and recvfrom](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answer without the host a zero-length receive on a stream socket (0, or `ENOTCONN` when it is not connected, [L1566-L1578](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1566-L1578)) and, on Windows only, a receive on a socket whose read side `shutdown` closed (0, as [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1556-L1562) returns at `SBS_CANTRCVMORE`); Winsock fails the second with `EPIPE` and blocks on the first. Linux and macOS still return data buffered before the `shutdown`, where FreeBSD discards it. `recvmsg` still asks the host in both cases. Not measured on a console. - [send, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - send and sendto without an address on an unconnected datagram socket map Windows `WSAENOTCONN` to FreeBSD's `EDESTADDRREQ` (39). -- [bind, connect, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) check a socket address as `bind` and `connect` did in a payload process on a PS5 (firmware 13.60): a length below 2 fails with `EINVAL` and above 255 with `ENAMETOOLONG`; on an `AF_INET` socket another family fails with `EAFNOSUPPORT` and an `AF_INET` address of any length but 16 with `EINVAL`; on an `AF_INET6` socket any length but 28 fails with `EINVAL`. `sendto`, and another family on an `AF_INET6` socket (`EAFNOSUPPORT`, after the length as FreeBSD 11.4's `in6_pcbbind` checks it), were not measured. `connect` on a listening stream socket fails with `EOPNOTSUPP` before the family and length checks, as FreeBSD 11.4's `soconnect` does; that order was not measured either +- [bind, connect, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) check a socket address as a payload process on a PS5 (firmware 13.60) did: a length below 2 fails with `EINVAL` and above 255 with `ENAMETOOLONG`; an `AF_INET` socket needs exactly 16 bytes and an `AF_INET6` socket 28, or `EINVAL`, and another family fails with `EAFNOSUPPORT`. On an `AF_INET` socket `bind` and `connect` check the family first and `sendto` the length first; an `AF_INET6` socket checks the length first for all three. `connect` on a listening stream socket fails with `EOPNOTSUPP` before these checks, as FreeBSD 11.4's `soconnect` does; that order was not measured - [read, write and vector I/O](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. `EPIPE` from a write raises the host's `SIGPIPE` on Linux unless the socket has `SO_NOSIGPIPE` set, and the guest cannot ignore it: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; nothing is raised on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` - [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number From d6433a6006a03894c7394f363855329ef453fb88 Mon Sep 17 00:00:00 2001 From: Himansh Raj Date: Sat, 24 Oct 2026 00:50:46 +0530 Subject: [PATCH 144/428] build: compile the shared libc headers, thread priority and three tests on macOS A macOS build of main failed 49 objects. Most included libc/include/FileStream.hpp, whose global-namespace ::feof_unlocked and ::ferror_unlocked do not exist there (they are macros in the macOS SDK). On macOS: - FileStream.hpp calls feof_unlocked and ferror_unlocked unqualified. - ExportMacros.hpp adds the leading underscore Mach-O gives C symbols to both names in APS5_EXPORT, so the .set alias resolves. - ThreadPriority.cpp has no rtkit or RLIMIT_RTTIME; "high" asks for the user-interactive QoS class and "rt" for a Mach time-constraint policy (period 1 ms, computation 0.5 ms), documented in ENVIRONMENT.md. - Formatting.cpp maps the SysV va_list builtins to va_list, and GuestContentSearch.cpp and GuestNpWebApi2.cpp include . With these, 16 objects still fail on macOS; each is in a file changed by an open macOS PR. Linux and Windows compile the same code as before. --- .../Execution/src/ThreadPriority.cpp | 31 ++++++++++++++++++- core/libs/prx/libc/include/FileStream.hpp | 3 ++ .../prx/libc/include/general/ExportMacros.hpp | 6 +++- core/libs/tests/Formatting.cpp | 6 ++++ core/libs/tests/GuestContentSearch.cpp | 3 ++ core/libs/tests/GuestNpWebApi2.cpp | 3 ++ docs/dev/ENVIRONMENT.md | 2 +- 7 files changed, 51 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp index 2db74fc43c..e88d6475f4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp @@ -12,6 +12,12 @@ #ifdef _WIN32 #include +#elif defined(__APPLE__) +#include +#include +#include +#include +#include #else #include #include @@ -42,7 +48,30 @@ void LogOnce(const char* role, const std::string& text) { std::fprintf(stderr, "[thread-priority] %s: %s\n", role, text.c_str()); } -#ifndef _WIN32 +#if defined(__APPLE__) + +void Raise(const char* role) { + if (RequestedMode() == ThreadPriorityMode::Realtime) { + mach_timebase_info_data_t timebase{}; + mach_timebase_info(&timebase); + const auto ticks = [&](std::uint64_t nanos) { + return static_cast(nanos * timebase.denom / timebase.numer); + }; + thread_time_constraint_policy_data_t policy{ticks(1000000), ticks(500000), ticks(1000000), TRUE}; + const auto status = thread_policy_set(pthread_mach_thread_np(pthread_self()), THREAD_TIME_CONSTRAINT_POLICY, + reinterpret_cast(&policy), THREAD_TIME_CONSTRAINT_POLICY_COUNT); + if (status != KERN_SUCCESS) + throw std::runtime_error(std::string("APS5_THREAD_PRIORITY=rt: ") + role + " time-constraint policy refused (kern_return " + std::to_string(status) + ")"); + LogOnce(role, "Mach time-constraint policy"); + return; + } + const int error = pthread_set_qos_class_self_np(QOS_CLASS_USER_INTERACTIVE, 0); + if (error != 0) + throw std::runtime_error(std::string("APS5_THREAD_PRIORITY=high: ") + role + " user-interactive QoS refused: " + std::strerror(error)); + LogOnce(role, "user-interactive QoS"); +} + +#elif !defined(_WIN32) struct DBusConnection; struct DBusMessage; diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index 3f1d90eb88..361fcb455a 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -230,6 +230,9 @@ public: #ifdef _WIN32 const bool endOfFile = std::feof(GetHandle()) != 0; const bool error = std::ferror(GetHandle()) != 0; +#elif defined(__APPLE__) + const bool endOfFile = feof_unlocked(GetHandle()) != 0; + const bool error = ferror_unlocked(GetHandle()) != 0; #else const bool endOfFile = ::feof_unlocked(GetHandle()) != 0; const bool error = ::ferror_unlocked(GetHandle()) != 0; diff --git a/core/libs/prx/libc/include/general/ExportMacros.hpp b/core/libs/prx/libc/include/general/ExportMacros.hpp index 49ab7fbf8c..c175c74f2b 100644 --- a/core/libs/prx/libc/include/general/ExportMacros.hpp +++ b/core/libs/prx/libc/include/general/ExportMacros.hpp @@ -1,7 +1,11 @@ #ifndef CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_EXPORTMACROS_HPP #define CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_EXPORTMACROS_HPP -#if defined(__GNUC__) || defined(__clang__) +#if defined(__APPLE__) + #define APS5_EXPORT(exportName, funcName) \ + __asm__(".globl \"_" exportName "_nid_no_patch_cut\"\n\t" \ + ".set \"_" exportName "_nid_no_patch_cut\", _" #funcName) +#elif defined(__GNUC__) || defined(__clang__) #define APS5_EXPORT(exportName, funcName) \ __asm__(".globl \"" exportName "_nid_no_patch_cut\"\n\t" \ ".set \"" exportName "_nid_no_patch_cut\", " #funcName) diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index 937f348f17..da14d71c54 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -11,6 +11,12 @@ #include #include #include +#ifdef __APPLE__ +#include +#define __builtin_sysv_va_list va_list +#define __builtin_sysv_va_start va_start +#define __builtin_sysv_va_end va_end +#endif extern "C" { int APS5_VABI snprintf_nid_postfix(char*, size_t, const char*, ...); diff --git a/core/libs/tests/GuestContentSearch.cpp b/core/libs/tests/GuestContentSearch.cpp index 5676436c77..f1decde043 100644 --- a/core/libs/tests/GuestContentSearch.cpp +++ b/core/libs/tests/GuestContentSearch.cpp @@ -1,6 +1,9 @@ #include "SceTypes.hpp" #include #include +#ifdef __APPLE__ +#include +#endif #include extern "C" { diff --git a/core/libs/tests/GuestNpWebApi2.cpp b/core/libs/tests/GuestNpWebApi2.cpp index 1adccd989d..007a75c4c3 100644 --- a/core/libs/tests/GuestNpWebApi2.cpp +++ b/core/libs/tests/GuestNpWebApi2.cpp @@ -2,6 +2,9 @@ #include #include #include +#ifdef __APPLE__ +#include +#endif #include #include diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index d14be69b73..10fefe9657 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -394,7 +394,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_TEMPLATE_AUDIT_MS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 1000 | milliseconds after which a cached shader-resource object takes the full walk instead of the fast proof; `0` turns the audit off | | `APS5_TEXTURE_CACHE_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 0 | forced byte budget in MiB for the storage and sampled texture caches; `0` selects the budget automatically | | `APS5_THREAD_NICE` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | -10 | nice value the raised queue workers and presenter ask rtkit for (clamped to -20..19) | -| `APS5_THREAD_PRIORITY` | string | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | off | raises the queue workers and presenter above the title's threads: `off`, `high` or `rt` | +| `APS5_THREAD_PRIORITY` | string | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | off | raises the queue workers and presenter above the title's threads: `off`, `high` or `rt`; on macOS `high` is the user-interactive QoS class and `rt` a Mach time-constraint policy (period 1 ms, computation 0.5 ms) | | `APS5_THREAD_RT` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | 10 | realtime priority the raised threads ask rtkit for (clamped to 1..99) | | `APS5_THREAD_RTTIME_US` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | 200000 | hard RLIMIT_RTTIME the realtime plan lowers to, in microseconds | | `APS5_TRACE_BARRIERS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | prints that many simulated hazard barriers before stopping | From d9fde17a3d7f5641d6fc34d5001dabb1bf4f1bf1 Mon Sep 17 00:00:00 2001 From: Bram910 Date: Fri, 23 Oct 2026 21:32:52 +0200 Subject: [PATCH 145/428] fix(tools): validate trace summary JSONL records --- tools/tests/test_trace_digest.py | 80 ++++++++++++++++++++++++++++++++ tools/trace_digest.py | 19 +++++++- 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index 7df5e64fae..a0a12aab2c 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -46,6 +46,86 @@ STACKS = "# samples=5 depth=64 truncated=1\n# module a.prx 0x1000 0x1\nntdll.dll class TraceDigestTests(unittest.TestCase): + def test_summary_jsonl_record_shapes_are_validated(self): + path = Path(self.directory.name) / "invalid-summary.jsonl" + for record in (None, True, False, 7, 1.5, "", "interval_start_s", [], ["interval_start_s"]): + for mode in ("capture", "intervals"): + with self.subTest(record=record, mode=mode): + path.write_text("{}\n\n" + json.dumps(record) + "\n", encoding="utf-8") + with self.assertRaises(ValueError) as raised: + if mode == "capture": + trace_digest.load_summary(path, self.trace) + else: + trace_digest.load_intervals(path) + self.assertEqual(str(raised.exception), f"{path}:3: summary record must be an object") + + def test_summary_jsonl_syntax_errors_name_the_source_line(self): + path = Path(self.directory.name) / "invalid-summary.jsonl" + for record in ("{", '{"trace": }', "{} trailing"): + for mode in ("capture", "intervals"): + with self.subTest(record=record, mode=mode): + path.write_text("{}\n\n" + record + "\n", encoding="utf-8") + with self.assertRaises(ValueError) as raised: + if mode == "capture": + trace_digest.load_summary(path, self.trace) + else: + trace_digest.load_intervals(path) + self.assertTrue(str(raised.exception).startswith(f"{path}:3: invalid JSON: "), str(raised.exception)) + + def test_cli_rejects_invalid_summary_records_without_partial_output(self): + summary = self.trace.with_name("aps5-summary.jsonl") + explicit = summary.with_name("explicit-\u03a9.jsonl") + invalid = [json.dumps(record) for record in (None, True, 7, "interval_start_s", [], ["interval_start_s"])] + for record in [*invalid, "{", "{} trailing"]: + data = (json.dumps(SUMMARY) + "\n\n" + record + "\n").encode("utf-8") + summary.write_bytes(data) + explicit.write_bytes(data) + for arguments, path in (([str(self.trace)], summary), ([str(self.trace), "--summary", str(explicit)], explicit), + ([str(summary)], summary)): + for output in ([], ["--json"]): + with self.subTest(record=record, arguments=arguments, output=output): + result = subprocess.run([sys.executable, trace_digest.__file__, *arguments, *output], + capture_output=True, text=True, encoding="utf-8", timeout=20) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertTrue(result.stderr.startswith(f"FAIL: {path}:3: "), result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual(result.stdout, "") + self.assertEqual(path.read_bytes(), data) + + def test_mixed_summary_jsonl_objects_preserve_selection_and_intervals(self): + path = self.trace.with_name("aps5-summary.jsonl") + expected = self.json_output(self.trace) + interval = interval_line(0.0, 30.0, 20.0, 5.0) + records = [{}, dict(SUMMARY, trace="other.json.gz"), SUMMARY, interval, {"unrelated": "\u03a9"}] + text = "\r\n \t\r\n" + "\r\n\r\n".join(json.dumps(record) for record in records) + "\r\n" + path.write_text(text, encoding="utf-8-sig") + self.assertEqual(self.json_output(self.trace), expected) + self.assertEqual(trace_digest.load_intervals(path), [interval]) + records.extend([dict(SUMMARY, commit="latest"), dict(SUMMARY, trace=self.trace.with_suffix("").name, commit="alternate")]) + path.write_text("\n".join(json.dumps(record) for record in records), encoding="utf-8") + self.assertEqual(trace_digest.load_summary(path, self.trace)["commit"], "latest") + self.assertEqual(trace_digest.load_summary(path, self.trace.with_suffix(""))["commit"], "alternate") + + def test_empty_summary_jsonl_files_remain_valid(self): + path = self.trace.with_name("aps5-summary.jsonl") + for text in ("", "\n \t\r\n"): + for encoding in ("utf-8", "utf-8-sig"): + with self.subTest(text=text, encoding=encoding): + path.write_text(text, encoding=encoding) + self.assertIsNone(trace_digest.load_summary(path, self.trace)) + self.assertEqual(trace_digest.load_intervals(path), []) + + def test_unicode_separators_inside_summary_strings_are_not_record_boundaries(self): + path = self.trace.with_name("aps5-summary.jsonl") + for separator in ("\u0085", "\u2028", "\u2029"): + with self.subTest(separator=separator): + gpu = f"GPU{separator}name" + summary = dict(SUMMARY, gpu=gpu) + interval = interval_line(0.0, 30.0, 20.0, 5.0, gpu=gpu) + path.write_text("\n".join(json.dumps(record, ensure_ascii=False) for record in (summary, interval)), encoding="utf-8") + self.assertEqual(trace_digest.load_summary(path, self.trace), summary) + self.assertEqual(trace_digest.load_intervals(path), [interval]) + def test_cli_rejects_invalid_event_container_shapes_without_partial_output(self): cases = [(value, "trace must be an event array or object") for value in (None, True, 7, "trace")] cases += [({"traceEvents": value}, "traceEvents must be an array") diff --git a/tools/trace_digest.py b/tools/trace_digest.py index d259fbb62d..e8eb8e0766 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -93,11 +93,26 @@ def load_trace(path): return Trace(json.load(handle)) +def load_summary_lines(path): + records = [] + for number, line in enumerate(Path(path).read_text(encoding="utf-8-sig").split("\n"), 1): + if not line.strip(): + continue + try: + record = json.loads(line) + except json.JSONDecodeError as error: + raise ValueError(f"{path}:{number}: invalid JSON: {error.msg}") from error + if not isinstance(record, dict): + raise ValueError(f"{path}:{number}: summary record must be an object") + records.append(record) + return records + + def load_summary(path, trace_path): candidate = Path(path) if path else Path(trace_path).with_name("aps5-summary.jsonl") if not candidate.exists(): return None - lines = [json.loads(line) for line in candidate.read_text(encoding="utf-8-sig").splitlines() if line.strip()] + lines = load_summary_lines(candidate) name = Path(trace_path).name alternate = name[:-3] if name.endswith(".gz") else name + ".gz" for filename in (name, alternate): @@ -284,7 +299,7 @@ def markdown(result): def load_intervals(path): - lines = [json.loads(line) for line in Path(path).read_text(encoding="utf-8-sig").splitlines() if line.strip()] + lines = load_summary_lines(path) return [line for line in lines if "interval_start_s" in line] From 8cf0a39f86293f7ed7c326b6c295f240f93f6345 Mon Sep 17 00:00:00 2001 From: krishalivaibhav Date: Sat, 24 Oct 2026 01:03:32 +0530 Subject: [PATCH 146/428] fix(agc): let imports that cannot write guest memory keep the watch armed ImportWatched opened a blanket host-write window over the whole import range on macOS, so every block in it read as CPU-written even when the import stored to none of it, which failed agc_write_tracking (#10344). Add importMayWriteGuest, defaulting to the previous behaviour. The two GuestBufferMemory callers create Vulkan imports in-process and never write the range through a kernel API, so they opt out and only pages actually stored to are marked. Co-Authored-By: Claude Sonnet 5.5 --- .../libSceAgcDriver/Execution/include/GuestMemory.hpp | 2 +- .../prx/libSceAgcDriver/Execution/src/GuestMemory.cpp | 9 +++++++-- .../libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp | 4 ++-- core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index 0e9c7b751a..44b2a24a21 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -63,7 +63,7 @@ void WriteChanged(std::uint64_t address, std::span current, std bool WriteWatched(); void Unwatch(std::uint64_t address, std::size_t bytes); void Rewatch(std::span> ranges); -bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function& import, bool staysWatched = true); +bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function& import, bool staysWatched = true, bool importMayWriteGuest = true); void SetUnwatchHook(void (*hook)(std::uint64_t address, std::size_t bytes)); #ifdef _WIN32 using FreshCollectHook = bool (*)(std::uintptr_t address, std::size_t bytes, void (*written)(void*, std::uintptr_t, std::uintptr_t), void* context); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 960cf4a11c..60db50325f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -1673,7 +1674,7 @@ void Rewatch(std::span> ranges) { #endif } -bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function& import, bool staysWatched) { +bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function& import, bool staysWatched, bool importMayWriteGuest) { if (bytes == 0 || bytes > std::numeric_limits::max() - address) return import(); if (!staysWatched) { constexpr std::uint64_t page = 4096; @@ -1693,7 +1694,11 @@ bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function const bool covered = tracker.covers(first, static_cast(stop - first)); const auto runImport = [&] { #ifdef __APPLE__ - const GuestArena::HostWrite open(reinterpret_cast(first), static_cast(stop - first)); + std::optional open; + if (importMayWriteGuest) open.emplace(reinterpret_cast(first), static_cast(stop - first)); +#endif +#ifndef __APPLE__ + static_cast(importMayWriteGuest); #endif return import(); }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 77771c65bc..57ffdb2a07 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1833,13 +1833,13 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import } #endif return step == nullptr; - }, watchedHostPointer); + }, watchedHostPointer, false); #ifndef _WIN32 if (BackingFile backing; step != nullptr && watchedDmaBuf != watchedHostPointer && sharedBacking(context, entry, backing)) { GuestMemory::ImportWatched(base, bytes, [&] { step = createBackingImport(context, entry, backing, result); return step == nullptr; - }, watchedDmaBuf); + }, watchedDmaBuf, false); } if (step == nullptr && entry.dmaBuf && bytes >= settleFloorBytes && context.settleQueue != VK_NULL_HANDLE && context.settleQueueMutex != nullptr) { static const bool traceSettle = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr; diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index 267c945d64..17a83ae883 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -1140,7 +1140,7 @@ void CheckImportWindow() { bytes[0x100] = 0x11; bytes[Block + 0x2000] = 0x22; return true; - }), "the import window import failed"); + }, true, false), "the import window import failed"); Require(Watched(base, 3 * Block), "an import window left its range unwatched"); const std::array generations{synced, synced, synced}; std::array changed{}; From e393705239a3104846c1a91548e7c1fc9a2f9a38 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Fri, 23 Oct 2026 21:55:55 +0200 Subject: [PATCH 147/428] fix(agc): retire native depth before binding an overlapping layout --- .../Graphics/src/DepthSurface.cpp | 21 ++++------ .../execution/NativeMultisampleDepth.cpp | 40 +++++++++++++++++-- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 458af384d5..d5c8909cf3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -1963,20 +1963,15 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, const bool created = surface == nullptr; GuestAllocations::Lease creationLease; if (created) { - for (const auto& surface : surfaces()) { - if (!surface->active->load(std::memory_order_acquire) || !surface->samplingActive || surface->context.device != context.device) continue; - const auto bytes = target.samples == 1u ? DepthSliceBytes(target.extent, target.format == VK_FORMAT_D16_UNORM ? 2u : 4u) : MultisampledDepthBytes_nid_no_patch(target.extent, target.format == VK_FORMAT_D16_UNORM ? 2u : 4u, target.samples); - Require(target.address <= std::numeric_limits::max() - bytes && target.htileAddress <= std::numeric_limits::max() - HtileMetadataBytes(target.extent), "native depth attachment range overflows"); - for (const auto& range : surface->sampling.initial) { - Require(target.address >= range.address + range.bytes || range.address >= target.address + bytes, "native multisample depth attachment layout or metadata changed"); - if (target.stencilAddress != 0u) { - const auto stencilBytes = target.samples == 1u ? DepthSliceBytes(target.extent, 1u) : MultisampledDepthBytes_nid_no_patch(target.extent, 1u, target.samples); - Require(target.stencilAddress <= std::numeric_limits::max() - stencilBytes && (target.stencilAddress >= range.address + range.bytes || range.address >= target.stencilAddress + stencilBytes), "native multisample stencil attachment aliases a resident depth surface"); - } - if (target.htileAddress != 0u) Require(target.htileAddress >= range.address + range.bytes || range.address >= target.htileAddress + HtileMetadataBytes(target.extent), "native multisample depth attachment layout or metadata changed"); - } - } + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + const auto depthBytes = target.samples == 1u ? DepthSliceBytes(target.extent, d16 ? 2u : 4u) : MultisampledDepthBytes_nid_no_patch(target.extent, d16 ? 2u : 4u, target.samples); + const auto stencilBytes = target.stencilAddress == 0u ? 0u : target.samples == 1u ? DepthSliceBytes(target.extent, 1u) : MultisampledDepthBytes_nid_no_patch(target.extent, 1u, target.samples); + const auto metadataBytes = target.htileAddress == 0u ? 0u : HtileMetadataBytes(target.extent); + Require(target.address <= std::numeric_limits::max() - depthBytes && target.stencilAddress <= std::numeric_limits::max() - stencilBytes && target.htileAddress <= std::numeric_limits::max() - metadataBytes, "native depth attachment range overflows"); lock.unlock(); + RequireNoResidentDepthRead_nid_no_patch(target.address, static_cast(depthBytes)); + if (stencilBytes != 0u) RequireNoResidentDepthRead_nid_no_patch(target.stencilAddress, static_cast(stencilBytes)); + if (metadataBytes != 0u) RequireNoResidentDepthRead_nid_no_patch(target.htileAddress, static_cast(metadataBytes)); auto sampling = target.samples == 1u ? DepthSamplingState{} : prepareDepthSampling(context, target); creationLease = target.samples == 1u || !sampling.refusal.empty() ? GuestAllocations::Lease{} : GuestAllocations::GuestAllocationsAcquireAll_nid_postfix(); lock.lock(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index 641eaf93b0..460f76f800 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -672,6 +672,39 @@ void CheckDrawFeedback() { std::puts("Native depth feedback refused through the draw resource path; a written surface refused and a depth-tested one retired on guest read"); } +void CheckDepthAttachmentAlias() { + const auto device = OpenVulkanTestDevice(); + Require(device != nullptr, "native depth attachment alias requires Vulkan"); + const auto context = device->graphicsContext(); + constexpr VkExtent2D extent{5u, 3u}; + constexpr std::uint32_t samples = 4u; + const auto bytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, 4u, samples)); + TargetMemory memory(bytes), firstMetadata(65536u), secondMetadata(65536u); + std::memset(memory.Data(), 0x3c, bytes); + std::memset(firstMetadata.Data(), 0, 65536u); + std::memset(secondMetadata.Data(), 0, 65536u); + struct Cleanup { + AgcDriver::VulkanDevice& device; + ~Cleanup() { device.WaitIdle(); ClearCachedTextures(device.Device()); ClearDepthSurfaces(device.Device()); } + } cleanup{*device}; + DepthTarget first{memory.Address(), 0u, extent, VK_FORMAT_D32_SFLOAT, 0.0f, 0u, firstMetadata.Address(), false, samples}; + first.depthSwizzle = 24u; + first.iterate256 = first.htilePipeAligned = first.htileDepthOnly = true; + const auto firstView = DepthSurfaceView(context, first); + const auto words = DepthSampleWords(first); + auto kept = DepthSurfaceTexture(context, words, DecodeTextureResource(words), VkComponentMapping{}); + Require(kept != nullptr, "attachment alias fixture has no held native depth view"); + auto second = first; + second.extent.width = 6u; + second.htileAddress = secondMetadata.Address(); + Require(MultisampledDepthBytes_nid_no_patch(second.extent, 4u, samples) <= bytes, "attachment alias fixture exceeds its backing"); + const auto secondView = DepthSurfaceView(context, second); + Require(secondView != VK_NULL_HANDLE && secondView != firstView, "an aliased depth attachment reused the previous layout's view"); + Require(DepthSurfaceView(context, second) == secondView, "an unchanged native depth attachment was not reused"); + for (std::size_t offset = 0u; offset < bytes; ++offset) Require(memory.Data()[offset] == std::byte{0x3c}, "retiring an unwritten attachment alias changed guest bytes"); + std::puts("native depth attachment alias changes layout while retaining held GPU views"); +} + enum class ClearFault { None, PartialStore, CpuWrite, DepthWrite, BadMetadata, UnprovenDispatch, PendingWrite, Overlap, PossibleStore, CompletedPossibleStore, DriverDispatch, ImportWindowWrite }; void CheckClearDispatch() { @@ -927,10 +960,10 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, Require(probe[0] == std::byte{0xa5}, "resident depth guard blocked an unrelated read"); auto alias = target; alias.htileAddress += 65536u; - TargetRefuses([&] { DepthSurfaceView(context, alias); }, "layout or metadata changed"); + TargetRefuses([&] { DepthSurfaceView(context, alias); }, "sample-preserving guest publication"); alias = target; alias.htileDepthOnly = !target.htileDepthOnly; - TargetRefuses([&] { DepthSurfaceView(context, alias); }, "layout or metadata changed"); + TargetRefuses([&] { DepthSurfaceView(context, alias); }, "sample-preserving guest publication"); auto resource = DecodeTextureResource(words); for (std::uint32_t field = 0u; field < 18u; ++field) { auto changed = resource; @@ -1042,7 +1075,7 @@ void CheckHtileRouting(Context context, VkFormat format, std::uint32_t fragments auto alias = target; alias.samples = 1u; alias.address = single.Address(); - TargetRefuses([&] { DepthSurfaceView(context, alias); }, "layout or metadata changed"); + Require(DepthSurfaceView(context, alias) != VK_NULL_HANDLE, "a single-sample bind could not retire an unwritten native HTILE owner"); Require(untouched(), "a single-sample bind over a native owner's HTILE stored expanded words into it"); std::printf("Native depth format=%u samples=%u: binds over the owner's HTILE left its words alone\n", static_cast(format), fragments); } @@ -1304,6 +1337,7 @@ int main(int argc, char** argv) { } CheckDrawFeedback(); const auto context = device->GetContext(); + if (argc == 1) CheckDepthAttachmentAlias(); std::unique_lock lock(AgcDriver::GuestMemory::GpuMutex()); Require(DepthSupported(context, VK_FORMAT_D32_SFLOAT, 4u), "the native depth regression requires sampled D32 with four samples"); if (argc == 2 && std::string_view(argv[1]) == "--clear-refusals") { From c9ace48eb71bf4b28e16265c3ae2bc387df543f4 Mon Sep 17 00:00:00 2001 From: Bram910 Date: Fri, 23 Oct 2026 22:05:30 +0200 Subject: [PATCH 148/428] fix(tools): report squash-merged PRs in main sync checks --- tools/check_main_sync.py | 4 +-- tools/tests/test_check_main_sync.py | 45 +++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/tools/check_main_sync.py b/tools/check_main_sync.py index f6d7532082..3bea583956 100644 --- a/tools/check_main_sync.py +++ b/tools/check_main_sync.py @@ -4,7 +4,7 @@ import subprocess import sys from pathlib import Path -PULL_REQUEST = re.compile(r"Merge pull request #(\d+)") +PULL_REQUEST = re.compile(r"Merge pull request #(\d+)|\(#(\d+)\)$") BASES = ("upstream/main", "origin/main", "main") @@ -47,7 +47,7 @@ def merged_pull_requests(repo, mergebase, base, paths): for line in lines(repo, "-c", "core.quotepath=off", "log", "--first-parent", "--format=%h %s", f"{mergebase}..{base}", "--", *paths): match = PULL_REQUEST.search(line) if match: - found[int(match.group(1))] = line.split(" ", 1)[0] + found[int(match.group(1) or match.group(2))] = line.split(" ", 1)[0] return found diff --git a/tools/tests/test_check_main_sync.py b/tools/tests/test_check_main_sync.py index a82463563e..418c49c1ff 100644 --- a/tools/tests/test_check_main_sync.py +++ b/tools/tests/test_check_main_sync.py @@ -121,6 +121,51 @@ class CheckMainSyncTests(unittest.TestCase): self.assertIn("1 file(s) changed on both sides", result.stdout) self.assertIn("#77", result.stdout) + def check_squash_merge(self, conflict): + self.branch("feature") + self.repo.write("shared.txt", "one\nmine\nthree\n" if conflict else "zero\none\ntwo\nthree\n") + self.repo.commit("feature edit") + self.repo.git("checkout", "-q", "main") + self.repo.git("checkout", "-q", "-b", "theirs") + self.repo.write("shared.txt", "one\ntheirs\nthree\n" if conflict else "one\ntwo\nthree\nfour\n") + self.repo.commit("their edit") + self.repo.git("checkout", "-q", "main") + self.repo.git("merge", "--squash", "-q", "theirs") + self.repo.commit("fix(test): change the shared file (#4242)") + revision = check_main_sync.git(self.repo.path, "rev-parse", "--short", "main").stdout.strip() + data = check_main_sync.report(self.repo.path, "main", "feature") + self.assertEqual(data["pulls"], {4242: revision}) + self.assertEqual(data["shared"], ["shared.txt"]) + self.assertEqual(data["conflicts"], ["shared.txt"] if conflict else []) + for strict in ((False,) if conflict else (False, True)): + with self.subTest(strict=strict): + result = run_tool(self.repo.path, "--head", "feature", *(["--strict"] if strict else [])) + self.assertEqual(result.returncode, 1 if conflict or strict else 0, result.stderr) + self.assertIn("merge: CONFLICT" if conflict else "merge: clean", result.stdout) + self.assertIn("merged pull requests that touched them: #4242", result.stdout) + self.assertNotIn("Traceback", result.stderr) + + def test_clean_squash_merge_names_the_pull_request(self): + self.check_squash_merge(False) + + def test_conflicting_squash_merge_names_the_pull_request(self): + self.check_squash_merge(True) + + def test_squash_numbers_must_end_the_subject_and_touch_shared_paths(self): + self.branch("feature") + self.repo.write("shared.txt", "zero\none\ntwo\nthree\n") + self.repo.commit("feature edit") + self.repo.git("checkout", "-q", "main") + self.repo.write("shared.txt", "one\ntwo\nthree\nfour\n") + self.repo.commit("fix(test): reference (#13) without merging it") + self.assertEqual(check_main_sync.report(self.repo.path, "main", "feature")["pulls"], {}) + self.repo.write("shared.txt", "one\ntwo\nthree\nfour\nfive\n") + self.repo.commit("fix(test): mention (#13) but merge (#4242)") + revision = check_main_sync.git(self.repo.path, "rev-parse", "--short", "main").stdout.strip() + self.repo.write("other.txt", "unrelated\n") + self.repo.commit("fix(test): an unrelated pull request (#9000)") + self.assertEqual(check_main_sync.report(self.repo.path, "main", "feature")["pulls"], {4242: revision}) + def test_a_subdirectory_reports_the_same_conflicts_and_pull_requests(self): (self.repo.path / "docs").mkdir() self.repo.write("docs/notes.txt", "x\n") From 2d2c58d8058b3df1aff89dce693ffcb765fb89c8 Mon Sep 17 00:00:00 2001 From: Bram910 Date: Fri, 23 Oct 2026 22:45:55 +0200 Subject: [PATCH 149/428] fix(io): stage long-named outputs with short temporary names --- core/relinker/io/src/FileWriter.cpp | 5 ++--- core/relinker/io/tests/FileWriterTests.cpp | 15 ++++++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/core/relinker/io/src/FileWriter.cpp b/core/relinker/io/src/FileWriter.cpp index 49df133411..10606d09bb 100644 --- a/core/relinker/io/src/FileWriter.cpp +++ b/core/relinker/io/src/FileWriter.cpp @@ -33,11 +33,10 @@ int createTemporary(const std::filesystem::path& target, const mode_t mode, std: constexpr std::string_view characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; std::random_device device; for (int attempt = 0; attempt < 100; ++attempt) { - std::string suffix = "."; + std::string suffix = ".anyps5-"; for (int index = 0; index < 6; ++index) suffix += characters[device() % characters.size()]; - temporary = target; - temporary += suffix; + temporary = target.parent_path() / suffix; const int descriptor = open(temporary.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, mode); if (descriptor >= 0 || errno != EEXIST) return descriptor; diff --git a/core/relinker/io/tests/FileWriterTests.cpp b/core/relinker/io/tests/FileWriterTests.cpp index 8ec3b79abd..7ab1724d30 100644 --- a/core/relinker/io/tests/FileWriterTests.cpp +++ b/core/relinker/io/tests/FileWriterTests.cpp @@ -40,15 +40,15 @@ std::vector siblings(const std::filesystem::path& output) { std::vector names; for (const auto& entry : std::filesystem::directory_iterator(output.parent_path())) { const auto name = entry.path().filename().string(); - if (name.starts_with(output.filename().string() + ".")) + if (name.starts_with(output.filename().string() + ".") || name.starts_with(".anyps5-")) names.push_back(name); } return names; } template -void requireRunningReplacement(const std::filesystem::path& directory, const TValue& data, const bool leftover) { - const auto output = directory / (leftover ? "leftover" : "running"); +void requireRunningReplacement(const std::filesystem::path& directory, const TValue& data, const bool leftover, const std::string& name = "running") { + const auto output = directory / (leftover ? "leftover" : name); if (leftover) std::ofstream(directory / "leftover.tmp", std::ios::binary) << "killed"; std::filesystem::copy_file(std::filesystem::read_symlink("/proc/self/exe"), output); @@ -107,8 +107,8 @@ void requireRunningReplacement(const std::filesystem::path& directory, const TVa } template -void requireFailedReplacement(const std::filesystem::path& directory, const TValue& data) { - const auto output = directory / "limited"; +void requireFailedReplacement(const std::filesystem::path& directory, const TValue& data, const std::string& name = "limited") { + const auto output = directory / name; std::ofstream(output, std::ios::binary) << "previous"; rlimit limit{}; getrlimit(RLIMIT_FSIZE, &limit); @@ -158,6 +158,11 @@ void requireReplacement(const TValue& data) { requireRunningReplacement(directory, data, false); requireRunningReplacement(directory, data, true); requireFailedReplacement(directory, data); + const auto maximumName = pathconf(directory.c_str(), _PC_NAME_MAX); + if (maximumName <= 0) + throw std::runtime_error("Cannot determine the output filename limit"); + requireFailedReplacement(directory, data, std::string(static_cast(maximumName), 'f')); + requireRunningReplacement(directory, data, false, std::string(static_cast(maximumName), 'r')); requireInPlaceWrites(directory, data); } catch (...) { std::filesystem::remove_all(directory, error); From 8251179fc2f026619055e33316795f2d1aed55b4 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Fri, 23 Oct 2026 16:52:02 -0400 Subject: [PATCH 150/428] fix(libc): return the upper 32 bits of the state from drand48 and erand48 as titles' libc.prx does drand48 and erand48 returned the whole 48-bit state over 2^48, as FreeBSD does. The libc.prx bundled with eleven titles returns its upper 32 bits over 2^32, so the low 16 bits never reach the result; both functions now do the same. The generator and the other rand48 functions are unchanged. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Random.cpp | 4 ++-- core/libs/tests/GuestRandomState.cpp | 12 ++++++++---- docs/dev/debt/libc.md | 1 + 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libc/src/Random.cpp b/core/libs/prx/libc/src/Random.cpp index 06cf1d789d..9cd2429917 100644 --- a/core/libs/prx/libc/src/Random.cpp +++ b/core/libs/prx/libc/src/Random.cpp @@ -118,7 +118,7 @@ std::uint64_t NextRand48(std::uint16_t* seed, const char* function) { extern "C" { double APS5_VABI erand48_nid_postfix(std::uint16_t* seed) { - return std::ldexp(static_cast(NextRand48(seed, __func__)), -48); + return std::ldexp(static_cast(NextRand48(seed, __func__) >> 16), -32); } std::int64_t APS5_VABI nrand48_nid_postfix(std::uint16_t* seed) { @@ -131,7 +131,7 @@ std::int64_t APS5_VABI jrand48_nid_postfix(std::uint16_t* seed) { double APS5_VABI drand48_nid_postfix() { std::lock_guard lock(g_randomLock); - return std::ldexp(static_cast(NextRand48(g_rand48Seed, __func__)), -48); + return std::ldexp(static_cast(NextRand48(g_rand48Seed, __func__) >> 16), -32); } std::int64_t APS5_VABI lrand48_nid_postfix() { diff --git a/core/libs/tests/GuestRandomState.cpp b/core/libs/tests/GuestRandomState.cpp index 71511241be..2b18cb339e 100644 --- a/core/libs/tests/GuestRandomState.cpp +++ b/core/libs/tests/GuestRandomState.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include #include @@ -56,10 +57,10 @@ static bool SeedIs(const std::uint16_t* seed, std::uint16_t low, std::uint16_t m static void CheckRand48() { std::uint16_t seed[3] = {0x330e, 0xabcd, 0x1234}; - Require(erand48_nid_postfix(seed) == 0x657eb7255101p-48 && SeedIs(seed, 0x5101, 0xb725, 0x657e), "erand48 first value"); + Require(erand48_nid_postfix(seed) == 0x657eb725p-32 && SeedIs(seed, 0x5101, 0xb725, 0x657e), "erand48 first value"); Require(nrand48_nid_postfix(seed) == 1804928587 && SeedIs(seed, 0x6378, 0x0c96, 0xd72a), "nrand48 second value"); Require(jrand48_nid_postfix(seed) == 1517566982 && SeedIs(seed, 0x2a23, 0x3c06, 0x5a74), "jrand48 third value"); - Require(erand48_nid_postfix(seed) == 0x72534abf62f2p-48 && SeedIs(seed, 0x62f2, 0x4abf, 0x7253), "erand48 fourth value"); + Require(erand48_nid_postfix(seed) == 0x72534abfp-32 && SeedIs(seed, 0x62f2, 0x4abf, 0x7253), "erand48 fourth value"); Require(nrand48_nid_postfix(seed) == 684387517 && SeedIs(seed, 0x8d15, 0xd97a, 0x5195), "nrand48 fifth value"); Require(jrand48_nid_postfix(seed) == 3807181130 && SeedIs(seed, 0xeffc, 0xf94a, 0xe2ec), "jrand48 keeps bit 31 without sign extension"); @@ -102,19 +103,22 @@ static void CheckRand48() { constexpr std::array consoleMrand48 {178800969, 1952030186, 3585512650, 1443049011, 2428758494, 7588830}; srand48_nid_postfix(1); for (const std::int64_t expected : consoleMrand48) Require(mrand48_nid_postfix() == expected, "mrand48 matches the console after srand48(1)"); + constexpr std::array consoleDrand48 {0x4435afd7p-32, 0xae99dc60p-32, 0x43f73108p-32}; + srand48_nid_postfix(7); + for (const double expected : consoleDrand48) Require(drand48_nid_postfix() == expected, "drand48 matches the titles' libc.prx after srand48(7)"); #ifndef _WIN32 std::uint16_t guest[3] = {0x1234, 0x5678, 0x9abc}; unsigned short host[3] = {0x1234, 0x5678, 0x9abc}; for (int round = 0; round < 64; ++round) { - Require(erand48_nid_postfix(guest) == ::erand48(host), "erand48 matches the host"); + Require(erand48_nid_postfix(guest) == std::ldexp(std::floor(::erand48(host) * 0x1p32), -32), "erand48 keeps the host's upper 32 bits"); Require(nrand48_nid_postfix(guest) == ::nrand48(host), "nrand48 matches the host"); Require(jrand48_nid_postfix(guest) == static_cast(::jrand48(host)), "jrand48 matches the host's 32 bits"); } ::srand48(1234567); srand48_nid_postfix(1234567); for (int round = 0; round < 32; ++round) { - Require(drand48_nid_postfix() == ::drand48(), "drand48 matches the host"); + Require(drand48_nid_postfix() == std::ldexp(std::floor(::drand48() * 0x1p32), -32), "drand48 keeps the host's upper 32 bits"); Require(lrand48_nid_postfix() == ::lrand48(), "lrand48 matches the host"); Require(mrand48_nid_postfix() == static_cast(::mrand48()), "mrand48 matches the host's 32 bits"); } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 0aa73ff2ef..267cc98f5d 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -90,6 +90,7 @@ - [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. - [jrand48 and mrand48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) return the high 32 bits of the new 48-bit state zero-extended to 64 bits, as the console's `libSceLibcInternal.sprx` does when called from a homebrew payload process on a PS5 (firmware 12.70): a result with bit 31 set comes back between 0x80000000 and 0xFFFFFFFF, as FreeBSD 11.0's [`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/jrand48.c#L23) and [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/mrand48.c#L25) return it; FreeBSD 11.1 and later cast it to `int32_t` ([`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/jrand48.c#L26), [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/mrand48.c#L28)), which makes it negative. `lrand48` and `nrand48` matched FreeBSD there. Whether a title's own libc does the same is unverified - [seed48 and lcong48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x6F460 and 0x6F420): the multiplier and addend are shared by the whole `rand48` family, including `erand48`, `nrand48` and `jrand48` on a caller's seed; `lcong48` sets them with the seed, and `srand48` and `seed48` restore 0x5DEECE66D and 0xB. Unlike FreeBSD, `seed48` returns a static copy of the new seed rather than the previous one. A null argument, which the console dereferences, throws +- [drand48 and erand48](../../../core/libs/prx/libc/src/Random.cpp) (libc) return the upper 32 bits of the new 48-bit state over 2^32, as the `libc.prx` bundled with eleven titles does on 10,000 values per function; FreeBSD returns all 48 bits. Whether the console's `libSceLibcInternal.sprx` copy does the same is unmeasured. - [lgamma and signgam](../../../core/libs/prx/libc/src/Math.cpp) (libc) - `lgamma` returns the host's `lgamma` and stores the sign of Γ(x) in the exported `signgam` (NID `5gOOC0kzW0c`, starting at 0 as in FreeBSD 11's [`s_signgam.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/s_signgam.c)) by the rules of [`e_lgamma_r.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/e_lgamma_r.c): -1 for -0 and for a negative non-integer whose floor is odd, 1 otherwise, including poles, infinities and NaN. Taken from the FreeBSD source, not measured on the console. The NID is in the aerolib symbol list; no title is known to read `signgam`. PPSA99004 imports `lgamma` - [sinhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `sinhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_sinhf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_sinhf.c) from before `__ldexp_expf`, which squares `expf(|x| / 2)` between `logf(FLT_MAX)` and the overflow threshold, calling msun's [`expm1f`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_expm1f.c) and [`expf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_expf.c), whose paths for the arguments it passes are inlined here. Past the threshold Sony's error helper sets `errno` to `ERANGE` before returning `x * 1e37`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits as this implementation for every finite argument up to the threshold, of both signs; it was not run on Linux. That the system `libSceLibcInternal`, which Ruffle (PPSA68091) and RetroArch (PPSA99169) import while bundling a zero-return `libc.prx`, has the same code is assumed - [acoshl, asinhl, atanhl, atan2l, cbrtl, erfl, erfcl, expm1l, hypotl, log1pl, log2l, lgammal, tgammal, j0f, j1f, jnf, y0f, y1f and ynf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return the host C library's results (the float Bessel functions through its double ones), not those of the console's own code, so the last bits can differ (`y0f`, `y1f`, `ynf` follow the console at `+-0`). As in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x56410), `lgammal` does not store the sign in `signgam`; `hypotl` returns infinity for an infinite argument even when the other is a NaN, as C11 F.10.4.3 requires. [hypot3, hypot3f and hypot3l](../../../core/libs/prx/libc/src/Math.cpp) follow that module (0x51D30, 0x520D0, 0x52420): the first NaN of x, y and z is returned unchanged, any infinity gives positive infinity, a zero argument gives the two-argument hypot of the other two, and otherwise the largest magnitude is combined with the hypot of the other two From 8c3de0cbd9baf2f02a8558076b49acb89165fff5 Mon Sep 17 00:00:00 2001 From: Kian Date: Fri, 23 Oct 2026 17:45:44 +1000 Subject: [PATCH 151/428] fix(web): reset batch selection when changing apps --- .github/workflows/web-tests.yml | 2 +- demo/web/app.mjs | 10 ++- demo/web/tests/app.test.mjs | 140 ++++++++++++++++++++++++++++++++ tools/test_web.py | 4 +- 4 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 demo/web/tests/app.test.mjs diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml index 8de6e8d91f..4c42ec63a0 100644 --- a/.github/workflows/web-tests.yml +++ b/.github/workflows/web-tests.yml @@ -36,4 +36,4 @@ jobs: node --check demo/web/output-directory.mjs node --check demo/web/worker.mjs - name: Test browser converter helpers - run: node --test demo/web/tests/batch.test.mjs demo/web/tests/output-directory.test.mjs + run: node --test demo/web/tests/app.test.mjs demo/web/tests/batch.test.mjs demo/web/tests/output-directory.test.mjs diff --git a/demo/web/app.mjs b/demo/web/app.mjs index 4e0b534145..491cb17017 100644 --- a/demo/web/app.mjs +++ b/demo/web/app.mjs @@ -262,12 +262,16 @@ async function selectInputs(inputs) { $("choose").onclick = () => { $("file").value = ""; $("file").click(); }; $("change").onclick = () => { if (working) return; + worker?.terminate(); + worker = null; if (downloadUrl) { URL.revokeObjectURL(downloadUrl); downloadUrl = null; } - file = null; - summary = null; + selectedInputs = []; + summaries = []; + inspectionComplete = false; $("file").value = ""; $("folder").value = ""; - for (const id of ["selection", "warnings", "error", "done", "download"]) $(id).hidden = true; + $("app-list").replaceChildren(); + for (const id of ["selection", "warnings", "error", "done", "download", "app-list"]) $(id).hidden = true; $("acknowledge").checked = false; $("drop").hidden = false; update(); diff --git a/demo/web/tests/app.test.mjs b/demo/web/tests/app.test.mjs new file mode 100644 index 0000000000..a97a239ad6 --- /dev/null +++ b/demo/web/tests/app.test.mjs @@ -0,0 +1,140 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { setImmediate } from "node:timers/promises"; +import { test } from "node:test"; + +class Element { + children = []; + listeners = new Map(); + value = ""; + checked = false; + disabled = false; + textContent = ""; + + constructor(hidden = false) { this.hidden = hidden; } + addEventListener(type, listener) { this.listeners.set(type, listener); } + removeAttribute(name) { delete this[name]; } + append(...children) { this.children.push(...children); } + replaceChildren(...children) { this.children = children; } +} + +async function setup(t, name) { + const html = readFileSync(new URL("../index.html", import.meta.url), "utf8"); + const elements = new Map([...html.matchAll(/<[^>]+\bid="([^"]+)"[^>]*>/gu)].map(match => [ + match[1], new Element(/\shidden(?:\s|>)/u.test(match[0])) + ])); + const get = id => { + assert.ok(elements.has(id), `Missing element: ${id}`); + return elements.get(id); + }; + const options = new Map(["disk", "zip", "memory"].map(value => [`[value="${value}"]`, new Element()])); + get("save").querySelector = selector => options.get(selector); + get("folder").webkitdirectory = true; + const workers = []; + class Worker { + messages = []; + terminated = false; + constructor() { workers.push(this); } + postMessage(message) { this.messages.push(message); } + terminate() { this.terminated = true; } + } + const globals = { + document: { + getElementById: get, + createElement: () => new Element(), + querySelector: selector => { + assert.equal(selector, 'input[name="target"]:checked'); + return { value: "linux" }; + } + }, + window: { isSecureContext: true, Worker, WebAssembly, ReadableStream, WritableStream, crypto: { subtle: {} } }, + Worker, + fetch: async () => ({ + ok: true, + json: async () => ({ tag: "test-release", platforms: { linux: { expandedBytes: 1024 }, windows: { expandedBytes: 1024 } } }) + }) + }; + for (const [key, value] of Object.entries(globals)) { + const original = Object.getOwnPropertyDescriptor(globalThis, key); + Object.defineProperty(globalThis, key, { value, configurable: true, writable: true }); + t.after(() => { + if (original) Object.defineProperty(globalThis, key, original); + else delete globalThis[key]; + }); + } + await import(new URL(`../app.mjs?${name}`, import.meta.url)); + await setImmediate(); + return { get, workers }; +} + +async function inspect(worker, count) { + for (let index = 0; index < count; index++) { + await worker.onmessage({ data: { + type: "inspected", index, + summary: { files: 1, totalBytes: 1024, modules: 0, selectedBytes: 1024, windowsSelectedBytes: 1024, warnings: ["Test warning"] } + } }); + } + await worker.onmessage({ data: { type: "inspection-complete" } }); +} + +for (const kind of ["zip", "batch", "folder"]) { + test(`Change resets an inspected ${kind} and permits a new conversion`, async t => { + const { get, workers } = await setup(t, kind); + assert.equal(get("convert").disabled, true); + if (kind === "folder") { + get("folder").files = [{ name: "eboot.bin", webkitRelativePath: "app/eboot.bin" }]; + get("folder").onchange(); + } else { + get("file").files = kind === "batch" ? [{ name: "first.zip" }, { name: "second.zip" }] : [{ name: "first.zip" }]; + get("file").onchange(); + } + const first = workers[0]; + assert.equal(first.messages[0].type, "inspect"); + assert.equal(first.messages[0].inputs.length, kind === "batch" ? 2 : 1); + get("change").onclick(); + assert.equal(first.terminated, false); + await inspect(first, first.messages[0].inputs.length); + assert.equal(get("selection").hidden, false); + assert.equal(get("drop").hidden, true); + assert.equal(get("warnings").hidden, false); + assert.equal(get("app-list").children.length, first.messages[0].inputs.length); + assert.equal(get("app-list").hidden, kind !== "batch"); + get("acknowledge").checked = true; + get("acknowledge").listeners.get("change")(); + assert.equal(get("convert").disabled, false); + + get("change").onclick(); + assert.equal(first.terminated, true); + assert.equal(get("file").value, ""); + assert.equal(get("folder").value, ""); + assert.equal(get("selection").hidden, true); + assert.equal(get("warnings").hidden, true); + assert.equal(get("app-list").hidden, true); + assert.equal(get("app-list").children.length, 0); + assert.equal(get("drop").hidden, false); + assert.equal(get("acknowledge").checked, false); + assert.equal(get("convert").disabled, true); + get("acknowledge").checked = true; + get("acknowledge").listeners.get("change")(); + assert.equal(get("convert").disabled, true); + get("change").onclick(); + + get("file").files = [{ name: "replacement.zip" }]; + get("file").onchange(); + assert.equal(workers.length, 2); + const replacement = workers[1]; + assert.deepEqual(replacement.messages[0].inputs.map(input => input.name), ["replacement.zip"]); + assert.equal(get("convert").disabled, true); + await inspect(replacement, 1); + assert.equal(get("archive-name").textContent, "replacement.zip"); + assert.equal(get("app-list").children.length, 1); + assert.equal(get("app-list").hidden, true); + assert.equal(get("convert").disabled, true); + get("acknowledge").checked = true; + get("acknowledge").listeners.get("change")(); + assert.equal(get("convert").disabled, false); + await get("convert").onclick(); + assert.equal(replacement.messages.at(-1).type, "convert"); + assert.equal(replacement.terminated, false); + }); +} diff --git a/tools/test_web.py b/tools/test_web.py index 9da700335f..03fe29a0c7 100644 --- a/tools/test_web.py +++ b/tools/test_web.py @@ -26,7 +26,9 @@ def main(argv=None): if not archive.is_file(): parser.error(f"archive module does not exist: {archive}") try: - subprocess.run(["node", "--test", str(ROOT / "demo/web/tests/output-directory.test.mjs")], check=True) + subprocess.run(["node", "--test", *(str(ROOT / "demo/web/tests" / name) for name in ( + "app.test.mjs", "batch.test.mjs", "output-directory.test.mjs" + ))], check=True) with tempfile.TemporaryDirectory(prefix="anyps5-web-") as directory: fixture = Path(directory) (fixture / "main.elf").write_bytes(main_fixture()) From 54d7c780a583309217117622a473388973e7f5ca Mon Sep 17 00:00:00 2001 From: Mehran Date: Thu, 22 Oct 2026 14:57:24 +0300 Subject: [PATCH 152/428] perf(shader): reuse a memoized result by patching its copied words --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +- .../libSceAgcDriver/tests/ShaderMemory.cpp | 5 +- core/shader/recompiler/Recompiler.cpp | 213 +++++++++++++++--- docs/dev/ENVIRONMENT.md | 2 + 4 files changed, 186 insertions(+), 38 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 5f76ec1792..3b1adbcd68 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2397,12 +2397,14 @@ if(BUILD_TESTING) set_tests_properties(agc_shader_srt_read_patching PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_shader_table_snapshot_reuse COMMAND agc_shader_memory_tests --table-snapshot-reuse) set_tests_properties(agc_shader_table_snapshot_reuse PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_shader_table_snapshot_reuse_unmasked COMMAND agc_shader_memory_tests --table-snapshot-reuse) + set_tests_properties(agc_shader_table_snapshot_reuse_unmasked PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_MASKED_RESULT_MEMO=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_driver_image_table_vouched_lookups COMMAND agc_driver_image_table_cache_bounds_tests --vouched-lookups) set_tests_properties(agc_driver_image_table_vouched_lookups PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_WRITE_WATCH_IMPORTS=watch" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 60) add_test(NAME agc_driver_image_table_vouch_queued_draws COMMAND agc_driver_image_table_cache_bounds_tests --queued-draws) set_tests_properties(agc_driver_image_table_vouch_queued_draws PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 60) if(WIN32) - set_tests_properties(agc_shader_table_snapshot_reuse agc_driver_image_table_vouched_lookups agc_driver_image_table_vouch_queued_draws PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_shader_table_snapshot_reuse agc_shader_table_snapshot_reuse_unmasked agc_driver_image_table_vouched_lookups agc_driver_image_table_vouch_queued_draws PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_shader_type_zero_image_materialization COMMAND agc_shader_memory_tests --type-zero-image-materialization) set_tests_properties(agc_shader_type_zero_image_materialization PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index b8af5fe895..f8697501ac 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1950,7 +1950,10 @@ void verifyTableSnapshotReuse() { const auto otherBuffer = CompileTable(request); std::copy(outputV.begin(), outputV.end(), guest.srt.begin() + 12); const auto resolvedShared = ResourceMaterializer::TableReuse(); - require(otherBuffer.result != compiled.result && otherBuffer.result->variantId == compiled.result->variantId && resolvedShared.resolutionsShared - resolvedBefore.resolutionsShared == 1u && resolvedShared.resolutions == resolvedBefore.resolutions && TableMap(*otherBuffer.result) == map && TableBinding(*otherBuffer.result, DescriptorRole::ImageTable)->guestDescriptor == images->guestDescriptor, "table snapshot reuse: a result for another buffer did not reuse the unchanged table's resolution"); + const bool resolves = std::getenv("APS5_NO_MASKED_RESULT_MEMO") != nullptr || std::getenv("APS5_VERIFY_MASKED_RESULT_MEMO") != nullptr; + require(otherBuffer.result != compiled.result && otherBuffer.result->variantId == compiled.result->variantId && resolvedShared.resolutionsShared - resolvedBefore.resolutionsShared == (resolves ? 1u : 0u) && resolvedShared.resolutions == resolvedBefore.resolutions && TableMap(*otherBuffer.result) == map && TableBinding(*otherBuffer.result, DescriptorRole::ImageTable)->guestDescriptor == images->guestDescriptor, "table snapshot reuse: a result for another buffer did not reuse the unchanged table's resolution"); + const auto* otherBuffers = TableBinding(*otherBuffer.result, DescriptorRole::GuestBuffers); + require(otherBuffers != nullptr && std::search(otherBuffers->guestDescriptor.begin(), otherBuffers->guestDescriptor.end(), otherOutputV.begin(), otherOutputV.end()) != otherBuffers->guestDescriptor.end(), "table snapshot reuse: a result for another buffer does not hold that buffer's descriptor"); const auto storedWord = heap[5u * 8u]; heap[5u * 8u] = heap[6u * 8u]; const auto restoredMap = map; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 5ff25c3400..5d99e9d70d 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -348,11 +348,22 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat // A materialized result of one variant over one snapshot (Recompile(request, capture)): the // shared immutable object every later capture that reproduces the snapshot receives, so Populate // and the per-request copy run once per distinct snapshot. +struct MemoPatch { + static constexpr std::uint32_t NoBinding = ~0u; + std::vector>> buffers; + std::vector userWords; + std::uint32_t shaderData = NoBinding; + std::uint32_t flattenedSrt = NoBinding; + bool pushData = false; + bool valid = false; +}; + struct ResultMemoEntry { std::uint64_t variantId = 0; std::uint64_t hash = 0; std::shared_ptr result; std::uint64_t used = 0; + std::shared_ptr patch; }; struct EmissionFailure { @@ -382,6 +393,7 @@ struct SourceEntry : std::enable_shared_from_this { std::vector> variants; std::vector emissionFailures; std::unique_ptr memo; + std::unique_ptr maskedMemo; std::uint64_t memoClock = 0; }; @@ -1007,7 +1019,28 @@ const ResourceSnapshot& mergeTargetImage(const CompiledVariant& variant, const R return merged; } -RecompileResult materializeResult(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& captured, bool registrationDefaults = false) { +void recordMemoPatch(MemoPatch& patch, const DescriptorBindingPlan& plan, const CompiledVariant& variant) { + const auto& layout = variant.bindings.layout; + patch = {}; + for (std::size_t index = 0; index < plan.bindings.size(); ++index) { + const auto& binding = plan.bindings[index]; + const auto position = static_cast(index); + if (binding.descriptor.role == DescriptorRole::GuestBuffers) { + patch.buffers.push_back({position, binding.resources}); + } else if (binding.descriptor.role == DescriptorRole::FlattenedSrt) { + patch.flattenedSrt = position; + } else if (binding.descriptor.role == DescriptorRole::ShaderData) { + patch.shaderData = position; + } + } + patch.pushData = layout.UsesPushData(); + if (patch.pushData || plan.storageData) { + for (const auto reg : layout.userDataRegisters) patch.userWords.push_back(reg - variant.info.userDataBase); + } + patch.valid = layout.userDataRegisters.size() <= layout.DispatchThreadLimitDword() && (patch.userWords.empty() || patch.pushData || patch.shaderData != MemoPatch::NoBinding); +} + +RecompileResult materializeResult(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& captured, bool registrationDefaults = false, MemoPatch* patch = nullptr) { ResourceSnapshot merged; const auto& snapshot = mergeTargetImage(variant, request, captured, registrationDefaults, merged); RecompileResult result; @@ -1155,6 +1188,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil const auto& tables = *resolved; BindingAllocationResult bindings; DescriptorBindingBuilder{}.Populate(bindings, variant.bindings, *bindingPlan, variant.info.userDataBase, snapshot, partialThreads(request), &tables); + if (patch != nullptr) recordMemoPatch(*patch, *bindingPlan, variant); result.workgroupMemoryDwords = WorkgroupMemoryStrideDwords(variant.info.info); result.bindings = std::move(bindings.bindings); if (std::ranges::none_of(result.bindings, [](const DescriptorBinding& binding) { return binding.role == DescriptorRole::BdaPagetable || binding.role == DescriptorRole::FaultBuffer; })) result.bdaAbiVersion = 0u; @@ -1350,21 +1384,78 @@ bool ResultMemo() { return resultMemo; } -ResultMemoEntry* memoSet(SourceEntry& source, std::uint64_t index) { - if (source.memo == nullptr) return nullptr; - return source.memo->data() + static_cast((index ^ (index >> 32u)) % ResultMemoSets) * ResultMemoWays; +bool MaskedResultMemo() { + static const bool masked = std::getenv("APS5_NO_MASKED_RESULT_MEMO") == nullptr; + return masked; +} + +bool VerifyMaskedResultMemo() { + static const bool verify = std::getenv("APS5_VERIFY_MASKED_RESULT_MEMO") != nullptr; + return verify; +} + +ResultMemoEntry* memoSet(const std::unique_ptr& table, std::uint64_t index) { + if (table == nullptr) return nullptr; + return table->data() + static_cast((index ^ (index >> 32u)) % ResultMemoSets) * ResultMemoWays; } struct ResultMemoCounters { - std::atomic hits{0}, misses{0}, evictions{0}, populateNanoseconds{0}; + std::atomic hits{0}, patched{0}, misses{0}, evictions{0}, populateNanoseconds{0}, patchNanoseconds{0}, mismatches{0}; std::atomic lastReport{0}; }; +void applyMemoPatch(RecompileResult& result, const MemoPatch& patch, const ResourceSnapshot& snapshot) { + for (const auto& [binding, resources] : patch.buffers) { + auto& words = result.bindings.at(binding).guestDescriptor; + if (words.size() != resources.size() * 4u) throw std::runtime_error("a patched guest buffer binding changed its width"); + for (std::size_t element = 0; element < resources.size(); ++element) std::copy_n(snapshot.buffers.at(resources[element]).dwords.begin(), 4u, words.begin() + element * 4u); + } + if (!patch.userWords.empty()) { + if (patch.pushData) { + if (patch.userWords.size() * sizeof(std::uint32_t) > result.pushConstants.size()) throw std::runtime_error("patched user data lies outside the push data"); + for (std::size_t word = 0; word < patch.userWords.size(); ++word) std::memcpy(result.pushConstants.data() + word * sizeof(std::uint32_t), &snapshot.userData.at(patch.userWords[word]), sizeof(std::uint32_t)); + } else { + auto& words = result.bindings.at(patch.shaderData).guestDescriptor; + if (patch.userWords.size() > words.size()) throw std::runtime_error("patched user data lies outside the shader data"); + for (std::size_t word = 0; word < patch.userWords.size(); ++word) words[word] = snapshot.userData.at(patch.userWords[word]); + } + } + if (patch.flattenedSrt != MemoPatch::NoBinding) result.bindings.at(patch.flattenedSrt).guestDescriptor = snapshot.flattenedSrt; + result.poisonedSrtReads = static_cast(snapshot.srtPoison.size()) + snapshot.nullRootReads; +} + +const char* patchedResultDifference(const RecompileResult& patched, const RecompileResult& full) { + if (patched.bindings.size() != full.bindings.size()) return "binding count"; + if (patched.pushConstants != full.pushConstants) return "push constants"; + if (patched.specializationId != full.specializationId || patched.spirv.ContentHash() != full.spirv.ContentHash()) return "module"; + if (patched.poisonedSrtReads != full.poisonedSrtReads) return "poisoned SRT reads"; + if (patched.shaderDataUserWords != full.shaderDataUserWords) return "shader data user words"; + if (patched.specialization.size() != full.specialization.size()) return "specialization"; + for (std::size_t index = 0; index < patched.specialization.size(); ++index) { + if (patched.specialization[index].id != full.specialization[index].id || patched.specialization[index].value != full.specialization[index].value) return "specialization"; + } + for (std::size_t index = 0; index < patched.bindings.size(); ++index) { + const auto& left = patched.bindings[index]; + const auto& right = full.bindings[index]; + if (left.role != right.role || left.binding != right.binding || left.count != right.count) return "binding shape"; + if (left.guestDescriptor != right.guestDescriptor) return "binding words"; + } + return nullptr; +} + ResultMemoCounters& resultMemoCounters() { static ResultMemoCounters counters; return counters; } +void verifyPatchedResult(const RecompileResult& patched, const RecompileResult& full, std::uint64_t variantId) { + const auto* difference = patchedResultDifference(patched, full); + if (difference == nullptr) return; + resultMemoCounters().mismatches.fetch_add(1, std::memory_order_relaxed); + static std::atomic printed{0}; + if (const auto index = printed.fetch_add(1, std::memory_order_relaxed); index < 20u) std::fprintf(stderr, "[recompile] masked result memo mismatch %u: variant %016llx differs in its %s\n", index + 1u, static_cast(variantId), difference); +} + void reportResultMemo() { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; if (!profile) return; @@ -1377,16 +1468,20 @@ void reportResultMemo() { } if (now - last < 10'000'000'000ll || !counters.lastReport.compare_exchange_strong(last, now, std::memory_order_relaxed)) return; const auto hits = counters.hits.exchange(0, std::memory_order_relaxed); + const auto patched = counters.patched.exchange(0, std::memory_order_relaxed); const auto misses = counters.misses.exchange(0, std::memory_order_relaxed); const auto evictions = counters.evictions.exchange(0, std::memory_order_relaxed); const auto populate = counters.populateNanoseconds.exchange(0, std::memory_order_relaxed); - std::fprintf(stderr, "[recompile] result memo (10 s): %llu hits, %llu misses (%.1f%% hits), Populate %.1f us per miss / %.1f ms in total, %llu evictions\n", static_cast(hits), static_cast(misses), hits + misses != 0 ? 100.0 * static_cast(hits) / static_cast(hits + misses) : 0.0, misses != 0 ? static_cast(populate) / 1000.0 / static_cast(misses) : 0.0, static_cast(populate) / 1e6, static_cast(evictions)); + const auto patching = counters.patchNanoseconds.exchange(0, std::memory_order_relaxed); + const auto mismatches = counters.mismatches.exchange(0, std::memory_order_relaxed); + const auto total = static_cast(hits + patched + misses); + std::fprintf(stderr, "[recompile] result memo (10 s): %llu hits, %llu patched, %llu misses (%.1f%% hits, %.1f%% patched), Populate %.1f us per miss / %.1f ms in total, patch %.1f us each / %.1f ms in total, %llu evictions, %llu patch mismatches\n", static_cast(hits), static_cast(patched), static_cast(misses), total != 0.0 ? 100.0 * static_cast(hits) / total : 0.0, total != 0.0 ? 100.0 * static_cast(patched) / total : 0.0, misses != 0 ? static_cast(populate) / 1000.0 / static_cast(misses) : 0.0, static_cast(populate) / 1e6, patched != 0 ? static_cast(patching) / 1000.0 / static_cast(patched) : 0.0, static_cast(patching) / 1e6, static_cast(evictions), static_cast(mismatches)); } // Everything materializeResult reads besides the variant: the snapshot (the descriptor words, the // flattened SRT, the user data, the uniform fill) and, for the vertex family, the V# table the // attributes are resolved from. -std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapshot& snapshot) { +std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapshot& snapshot, bool masked) { std::uint64_t hash = 0xcbf29ce484222325ull; const auto mix = [&](std::uint64_t value) { hash ^= value; @@ -1404,11 +1499,28 @@ std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapsh for (std::uint32_t i = 0; i < value.dwordCount && i < value.dwords.size(); ++i) mix(value.dwords[i]); } }; - mixDescriptors(snapshot.buffers); + if (masked) { + mix(snapshot.buffers.size()); + for (const auto& value : snapshot.buffers) { + mix(value.dwordCount); + mix(value.availableDwordMask); + const bool present = value.dwords[2] != 0u && (value.dwords[0] != 0u || (value.dwords[1] & 0xffffu) != 0u); + mix(present ? 4u | (value.dwords[0] & 3u) : 0u); + mix(value.dwords[1] & 0xffff0000u); + for (std::uint32_t i = 3; i < value.dwordCount && i < value.dwords.size(); ++i) mix(value.dwords[i]); + } + } else { + mixDescriptors(snapshot.buffers); + } mixDescriptors(snapshot.images); mixDescriptors(snapshot.samplers); - mixWords(snapshot.flattenedSrt); - mixWords(snapshot.userData); + if (masked) { + mix(snapshot.flattenedSrt.size()); + mix(snapshot.userData.size()); + } else { + mixWords(snapshot.flattenedSrt); + mixWords(snapshot.userData); + } mix(static_cast(snapshot.uniformFill.kind)); mix(snapshot.uniformFill.resource); for (const auto stride : snapshot.uniformFill.groupStride) mix(stride); @@ -1467,16 +1579,16 @@ std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapsh // as good). `memoHit` reports the hit. std::shared_ptr materializePreparedMemoized(SourceEntry& source, const std::shared_ptr& variant, const RecompileRequest& request, const ResourceSnapshot& snapshot, bool cacheHit, bool* memoHit) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; - const auto hash = snapshotHash(request, snapshot); - std::uint64_t index = 0; + const auto variantId = variant->artifact.variantId; + const auto hash = snapshotHash(request, snapshot, false); + const auto index = (variantId * 0x9e3779b97f4a7c15ull) ^ hash; auto& counters = resultMemoCounters(); { std::lock_guard lock(source.mutex); - index = (variant->artifact.variantId * 0x9e3779b97f4a7c15ull) ^ hash; - if (auto* set = memoSet(source, index)) { + if (auto* set = memoSet(source.memo, index)) { for (std::size_t way = 0; way < ResultMemoWays; ++way) { auto& entry = set[way]; - if (entry.result == nullptr || entry.variantId != variant->artifact.variantId || entry.hash != hash) continue; + if (entry.result == nullptr || entry.variantId != variantId || entry.hash != hash) continue; entry.used = ++source.memoClock; counters.hits.fetch_add(1, std::memory_order_relaxed); if (memoHit != nullptr) *memoHit = true; @@ -1485,32 +1597,61 @@ std::shared_ptr materializePreparedMemoized(SourceEntry& } } } - const auto started = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - auto result = std::make_shared(materializeResult(*variant, request, snapshot)); - result->cacheHit = cacheHit; - if (profile) counters.populateNanoseconds.fetch_add(static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count()), std::memory_order_relaxed); - counters.misses.fetch_add(1, std::memory_order_relaxed); - std::shared_ptr shared = std::move(result); - { + const bool masking = MaskedResultMemo(); + const auto maskedHash = masking ? snapshotHash(request, snapshot, true) : 0u; + const auto maskedIndex = (variantId * 0x9e3779b97f4a7c15ull) ^ maskedHash; + std::shared_ptr donor; + std::shared_ptr donorPatch; + if (masking) { std::lock_guard lock(source.mutex); - if (source.memo == nullptr) source.memo = std::make_unique(); - auto* set = memoSet(source, index); - auto* victim = set; - bool present = false; - for (std::size_t way = 0; way < ResultMemoWays && !present; ++way) { - auto& entry = set[way]; - if (entry.result != nullptr && entry.variantId == variant->artifact.variantId && entry.hash == hash) { + if (auto* set = memoSet(source.maskedMemo, maskedIndex)) { + for (std::size_t way = 0; way < ResultMemoWays; ++way) { + auto& entry = set[way]; + if (entry.result == nullptr || entry.variantId != variantId || entry.hash != maskedHash) continue; entry.used = ++source.memoClock; - shared = entry.result; - present = true; - } else if (entry.used < victim->used) { - victim = &entry; + donor = entry.result; + donorPatch = entry.patch; + break; } } - if (!present) { - if (victim->result != nullptr) counters.evictions.fetch_add(1, std::memory_order_relaxed); - *victim = {variant->artifact.variantId, hash, shared, ++source.memoClock}; + } + const auto started = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; + std::shared_ptr result; + std::shared_ptr patch; + if (donor != nullptr) { + result = std::make_shared(*donor); + applyMemoPatch(*result, *donorPatch, snapshot); + if (VerifyMaskedResultMemo()) verifyPatchedResult(*result, materializeResult(*variant, request, snapshot), variantId); + if (profile) counters.patchNanoseconds.fetch_add(static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count()), std::memory_order_relaxed); + counters.patched.fetch_add(1, std::memory_order_relaxed); + } else { + if (masking) patch = std::make_shared(); + result = std::make_shared(materializeResult(*variant, request, snapshot, false, patch.get())); + if (profile) counters.populateNanoseconds.fetch_add(static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count()), std::memory_order_relaxed); + counters.misses.fetch_add(1, std::memory_order_relaxed); + } + result->cacheHit = cacheHit; + std::shared_ptr shared = std::move(result); + const auto insert = [&](std::unique_ptr& table, std::uint64_t at, std::uint64_t key, std::shared_ptr recipe, bool count) { + if (table == nullptr) table = std::make_unique(); + auto* set = memoSet(table, at); + auto* victim = set; + for (std::size_t way = 0; way < ResultMemoWays; ++way) { + auto& entry = set[way]; + if (entry.result != nullptr && entry.variantId == variantId && entry.hash == key) { + entry.used = ++source.memoClock; + return entry.result; + } + if (entry.used < victim->used) victim = &entry; } + if (count && victim->result != nullptr) counters.evictions.fetch_add(1, std::memory_order_relaxed); + *victim = {variantId, key, shared, ++source.memoClock, std::move(recipe)}; + return shared; + }; + { + std::lock_guard lock(source.mutex); + shared = insert(source.memo, index, hash, nullptr, true); + if (patch != nullptr && patch->valid) static_cast(insert(source.maskedMemo, maskedIndex, maskedHash, patch, false)); } reportResultMemo(); return shared; diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index d14be69b73..4df580bcac 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -36,6 +36,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_CODE_HASH_KEY` | presence | `core/shader/recompiler/CacheKey.hpp` | | appends every code word to the cache key instead of hashing the code | | `APS5_NO_FAILURE_MEMO` | presence | `core/shader/recompiler/Recompiler.cpp` | | rebuilds a program whose compilation failed instead of reusing the memoized failure | | `APS5_NO_IMAGE_TABLES` | presence | `core/shader/recompiler/Optimization/src/ResourceTracker.cpp` | | disables image tables, leaving table-loaded descriptors unplanned | +| `APS5_NO_MASKED_RESULT_MEMO` | presence | `core/shader/recompiler/Recompiler.cpp` | | materializes every result memo miss instead of patching the buffer descriptors, user data and flattened SRT of a cached result whose snapshot differs only in those words | | `APS5_NO_PERF_DISK_PLANS` | presence | `core/shader/recompiler/Recompiler.cpp` | | disables the disk cache of planning results | | `APS5_NO_PERF_DOM_INTERSECTION` | presence | `core/shader/recompiler/ControlFlow/src/Structurizer.cpp` | | computes dominator intersections into a fresh vector instead of intersecting in place | | `APS5_NO_PERF_ITERATION_SELECTION` | presence | `core/shader/recompiler/ControlFlow/src/Structurizer.cpp` | | recomputes the post-dominator sets when finding a loop's iteration-selection merge instead of reusing them | @@ -52,6 +53,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_TRACE_BDA` | presence | `core/shader/recompiler/Optimization/src/ResourceTracker.cpp` | | prints the first few accesses that make a program take the address-based path | | `APS5_TRACE_BUFFER_WRITTEN` | presence | `core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp` | | prints how many guest buffer elements a program may store to and how many it only loads | | `APS5_TRACE_IMAGE_TABLE` | presence | `core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp` | | traces image-table resolution | +| `APS5_VERIFY_MASKED_RESULT_MEMO` | presence | `core/shader/recompiler/Recompiler.cpp` | | also materializes every patched result memo hit, prints the first twenty whose patched result differs and counts them in the result memo report of `APS5_PROFILE_DRAW` | | `APS5_VERIFY_SRT_COMPACT` | presence | `core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp` | | walks every SRT descriptor a second time through the IR evaluator and throws when the two walks differ | | `APS5_VERIFY_SRT_TAPE` | presence | `core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp` | | walks a plan's runtime sources a second time through the evaluator, adopts its answers where the two differ and prints the first twenty differences | | `APS5_WAVE_LDS_SUBGROUP` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp` | | restores subgroup-scope LDS barriers for single-wave workgroups instead of workgroup scope | From d01910e3401e14bc7664cfe37f97f06e6feb408b Mon Sep 17 00:00:00 2001 From: Himansh Raj Date: Sat, 24 Oct 2026 04:04:15 +0530 Subject: [PATCH 153/428] fix(agc): keep the host-pointer import's write window On macOS the host-pointer import is copied into a Metal buffer when it is imported, and the HostWrite window around it is what marks those pages written so the driver re-syncs them. Without it the depth plane a draw writes back into guest memory is never re-synced, and agc_driver_depth_plane_buffer_memory reads stale data. Only the dma-buf import keeps the watch armed, which is all agc_write_tracking needs. Tested on an Apple M2 Pro (x86_64 under Rosetta 2, MoltenVK 1.4.2): agc_write_tracking and agc_driver_depth_plane_buffer_memory both pass. --- .../libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 57ffdb2a07..a4596ee4bb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1833,7 +1833,7 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import } #endif return step == nullptr; - }, watchedHostPointer, false); + }, watchedHostPointer); #ifndef _WIN32 if (BackingFile backing; step != nullptr && watchedDmaBuf != watchedHostPointer && sharedBacking(context, entry, backing)) { GuestMemory::ImportWatched(base, bytes, [&] { From 8da1a2dc0b27d109b597a4e1f39b31a2f142a702 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sat, 24 Oct 2026 00:49:12 +0200 Subject: [PATCH 154/428] fix(libSceNgs2): return the PS5 pan codes and accept NaN pan inputs sceNgs2PanGetVolumeMatrix now returns the PS5 error codes in the PS5 check order instead of throwing, and lets NaN angles and distances through as the PS5 module does. sceNgs2PanInit gives the 7-speaker degree defaults for 0 speakers without angles, stores NaN speaker angles and leaves unused slots untouched. Inputs on which the PS5 library never returns still throw. Co-Authored-By: Claude Sonnet 5.5 --- .../libSceNgs2.native/include/Ngs2Types.hpp | 3 + core/libs/prx/libSceNgs2.native/src/Pan.cpp | 40 +++++----- .../libSceNgs2.native/tests/Ngs2Render.cpp | 73 +++++++++++++++---- docs/dev/debt/libSceNgs2.native.md | 2 +- 4 files changed, 84 insertions(+), 34 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index 54093115a2..0d1db76012 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -25,6 +25,9 @@ static constexpr int SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT = static_cast(0 static constexpr int SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT = static_cast(0x804A8432); static constexpr int SCE_NGS2_ERROR_INVALID_PAN_UNIT_ANGLE = static_cast(0x804A8450); static constexpr int SCE_NGS2_ERROR_INVALID_PAN_SPEAKER = static_cast(0x804A8451); +static constexpr int SCE_NGS2_ERROR_INVALID_PAN_MATRIX_FORMAT = static_cast(0x804A8452); +static constexpr int SCE_NGS2_ERROR_INVALID_PAN_WORK = static_cast(0x804A8453); +static constexpr int SCE_NGS2_ERROR_INVALID_PAN_PARAM = static_cast(0x804A8454); static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_HANDLE = static_cast(0x804A8203); static constexpr int SCE_NGS2_ERROR_NULL_PARAM_LIST = static_cast(0x804A8360); static constexpr int SCE_NGS2_ERROR_INVALID_GEOM_LISTENER_ADDRESS = static_cast(0x804A8461); diff --git a/core/libs/prx/libSceNgs2.native/src/Pan.cpp b/core/libs/prx/libSceNgs2.native/src/Pan.cpp index e1a4d70548..b41ca734ff 100644 --- a/core/libs/prx/libSceNgs2.native/src/Pan.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Pan.cpp @@ -1,11 +1,9 @@ -#include #include #include #include #include #include #include -#include #include "prx/libc/include/General.hpp" #include "Ngs2Internal.hpp" @@ -57,14 +55,16 @@ static void AddBetweenSpeakers(const Ngs2PanWork& work, std::uint32_t speaker, f int Ngs2PanInit(Ngs2PanWork* work, const float* speaker_angles, float unit_angle, uint32_t num_speakers) { if (work == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; if (speaker_angles != nullptr && !IsUnitAngle(unit_angle)) return SCE_NGS2_ERROR_INVALID_PAN_UNIT_ANGLE; - const auto* layout = LayoutFor(num_speakers); - if (layout == nullptr) { - if (speaker_angles == nullptr && num_speakers == 0) APS5_INVALID_ARG_EX; - return SCE_NGS2_ERROR_INVALID_PAN_SPEAKER; + if (speaker_angles == nullptr && num_speakers == 0) { + Ngs2PanInit(work, nullptr, DEGREES, 7); + work->unit_angle = unit_angle == RADIANS ? RADIANS : DEGREES; + return SCE_NGS2_OK; } - *work = {}; - work->num_speakers = num_speakers; + const auto* layout = LayoutFor(num_speakers); + if (layout == nullptr) return SCE_NGS2_ERROR_INVALID_PAN_SPEAKER; if (speaker_angles == nullptr) { + *work = {}; + work->num_speakers = num_speakers; for (std::uint32_t i = 0; i < num_speakers; i++) work->speaker_angles[i] = layout->defaultDegrees[i]; work->speaker_angles[num_speakers] = layout->defaultDegrees[0] + DEGREES; work->unit_angle = DEGREES; @@ -76,39 +76,39 @@ int Ngs2PanInit(Ngs2PanWork* work, const float* speaker_angles, float unit_angle } for (std::uint32_t i = 0; i < num_speakers; i++) { float angle = speaker_angles[layout->inputOrder[i]]; - if (!std::isfinite(angle)) APS5_INVALID_ARG_EX; while (angle < 0.0f) { - if (angle + unit_angle == angle) throw std::runtime_error("NGS2: pan angles too large to wrap are not implemented"); + if (angle + unit_angle == angle) throw std::runtime_error("NGS2: pan speaker angles too large to wrap are not implemented (the PS5 library never returns for them)"); angle += unit_angle; } while (angle >= unit_angle) { - if (!(unit_angle < angle)) APS5_INVALID_ARG_EX; - if (angle - unit_angle == angle) throw std::runtime_error("NGS2: pan angles too large to wrap are not implemented"); + if (!(unit_angle < angle)) throw std::runtime_error("NGS2: a pan speaker angle that wraps to the unit angle is not implemented (the PS5 library never returns for it)"); + if (angle - unit_angle == angle) throw std::runtime_error("NGS2: pan speaker angles too large to wrap are not implemented (the PS5 library never returns for them)"); angle -= unit_angle; } work->speaker_angles[i] = angle; } work->speaker_angles[num_speakers] = work->speaker_angles[0] + unit_angle; work->unit_angle = unit_angle; + work->num_speakers = num_speakers; return SCE_NGS2_OK; } int Ngs2PanGetVolumeMatrix(Ngs2PanWork* work, const Ngs2PanParam* params, uint32_t num_params, uint32_t matrix_format, float* out_volume_matrix) { - if (work == nullptr || params == nullptr) APS5_INVALID_ARG_EX; + if (work == nullptr) return SCE_NGS2_ERROR_INVALID_PAN_WORK; + if (params == nullptr) return SCE_NGS2_ERROR_INVALID_PAN_PARAM; if (out_volume_matrix == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; - if (matrix_format != 1 && matrix_format != 2 && matrix_format != 6 && matrix_format != 8) APS5_INVALID_ARG_EX; + if (matrix_format != 1 && matrix_format != 2 && matrix_format != 6 && matrix_format != 8) return SCE_NGS2_ERROR_INVALID_PAN_MATRIX_FORMAT; const auto* layout = LayoutFor(work->num_speakers); - if (layout == nullptr || !IsUnitAngle(work->unit_angle) || !std::isfinite(work->speaker_angles[0]) || num_params > MAX_PAN_PARAMS) APS5_INVALID_ARG_EX; - for (std::uint32_t p = 0; p < num_params; p++) { - if (!std::isfinite(params[p].angle) || std::isnan(params[p].distance)) APS5_INVALID_ARG_EX; - } + if (layout == nullptr) return SCE_NGS2_ERROR_INVALID_PAN_SPEAKER; + if (!IsUnitAngle(work->unit_angle)) return SCE_NGS2_ERROR_INVALID_PAN_UNIT_ANGLE; + if (num_params > MAX_PAN_PARAMS) return SCE_NGS2_ERROR_INVALID_NUM_CHANNELS; std::memset(out_volume_matrix, 0, static_cast(num_params) * matrix_format * sizeof(float)); const std::uint32_t numSpeakers = work->num_speakers; const float unit = work->unit_angle; const float first = work->speaker_angles[0]; for (std::uint32_t p = 0; p < num_params; p++) { const auto& param = params[p]; - const float distance = std::max(-1.0f, std::min(1.0f, param.distance)); + const float distance = param.distance > 1.0f ? 1.0f : param.distance < -1.0f ? -1.0f : param.distance; const float towardWeight = std::sin((distance + 1.0f) * 0.5f * std::numbers::pi_v * 0.5f); const float oppositeWeight = std::sin((1.0f - distance) * 0.5f * std::numbers::pi_v * 0.5f); float angle = param.angle; @@ -116,7 +116,7 @@ int Ngs2PanGetVolumeMatrix(Ngs2PanWork* work, const Ngs2PanParam* params, uint32 const float before = angle; if (angle >= first + unit) angle -= unit; if (angle < first) angle += unit; - if (angle == before) throw std::runtime_error("NGS2: pan angles too large to wrap are not implemented"); + if (angle == before) throw std::runtime_error("NGS2: pan angles that do not wrap into one turn are not implemented (the PS5 library never returns for them)"); } float opposite = angle + unit * 0.5f; if (!(opposite < first + unit)) opposite -= unit; diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index c9f7cd7fcb..3aff39060e 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -243,17 +243,36 @@ static void TestPan() { Require(sceNgs2PanInit(&work, nullptr, 360.0f, 2) == SCE_NGS2_OK); Require(sceNgs2PanGetVolumeMatrix(&work, &front, 1, 2, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); Require(sceNgs2PanGetVolumeMatrix(&work, &front, 0, 2, out) == SCE_NGS2_OK); - Require(PanThrows(nullptr, &front, 1, 2) && PanThrows(&work, nullptr, 0, 2)); - Require(PanThrows(&work, &front, 1, 4) && PanThrows(&work, &front, 1, 3) && !PanThrows(&work, &front, 1, 8)); + float guard[32]; + const auto panResult = [&](Ngs2PanWork* panWork, const Ngs2PanParam* params, std::uint32_t numParams, std::uint32_t format) { + std::fill(std::begin(guard), std::end(guard), -1.0f); + const int result = sceNgs2PanGetVolumeMatrix(panWork, params, numParams, format, guard); + Require(result == SCE_NGS2_OK || std::all_of(std::begin(guard), std::end(guard), [](float level) { return level == -1.0f; })); + return result; + }; const Ngs2PanParam many[9] = {}; - Require(PanThrows(&work, many, 9, 2) && !PanThrows(&work, many, 8, 2)); + Require(panResult(nullptr, &front, 1, 2) == SCE_NGS2_ERROR_INVALID_PAN_WORK && panResult(nullptr, nullptr, 1, 2) == SCE_NGS2_ERROR_INVALID_PAN_WORK); + Require(sceNgs2PanGetVolumeMatrix(nullptr, &front, 1, 2, nullptr) == SCE_NGS2_ERROR_INVALID_PAN_WORK); + Require(panResult(&work, nullptr, 0, 2) == SCE_NGS2_ERROR_INVALID_PAN_PARAM && panResult(&work, nullptr, 1, 3) == SCE_NGS2_ERROR_INVALID_PAN_PARAM); + Require(sceNgs2PanGetVolumeMatrix(&work, nullptr, 1, 2, nullptr) == SCE_NGS2_ERROR_INVALID_PAN_PARAM); + Require(sceNgs2PanGetVolumeMatrix(&work, &front, 1, 3, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(panResult(&work, &front, 1, 4) == SCE_NGS2_ERROR_INVALID_PAN_MATRIX_FORMAT && panResult(&work, &front, 1, 3) == SCE_NGS2_ERROR_INVALID_PAN_MATRIX_FORMAT); + Require(panResult(&work, many, 9, 3) == SCE_NGS2_ERROR_INVALID_PAN_MATRIX_FORMAT && panResult(&work, &front, 1, 8) == SCE_NGS2_OK); + Require(panResult(&work, many, 9, 2) == SCE_NGS2_ERROR_INVALID_NUM_CHANNELS && panResult(&work, many, 8, 2) == SCE_NGS2_OK); + Ngs2PanWork badWork = work; + badWork.num_speakers = 3; + Require(panResult(&badWork, &front, 1, 2) == SCE_NGS2_ERROR_INVALID_PAN_SPEAKER && panResult(&badWork, &front, 1, 3) == SCE_NGS2_ERROR_INVALID_PAN_MATRIX_FORMAT); + Require(panResult(&badWork, many, 9, 2) == SCE_NGS2_ERROR_INVALID_PAN_SPEAKER); + badWork.unit_angle = 100.0f; + Require(panResult(&badWork, &front, 1, 2) == SCE_NGS2_ERROR_INVALID_PAN_SPEAKER); + badWork.num_speakers = 2; + Require(panResult(&badWork, &front, 1, 2) == SCE_NGS2_ERROR_INVALID_PAN_UNIT_ANGLE && panResult(&badWork, many, 9, 2) == SCE_NGS2_ERROR_INVALID_PAN_UNIT_ANGLE); + Require(sceNgs2PanGetVolumeMatrix(&badWork, &front, 1, 2, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); const Ngs2PanParam nanAngle{NAN, 1.0f, 1.0f, 0.0f}; - Require(PanThrows(&work, &nanAngle, 1, 2)); + Require(panResult(&work, &nanAngle, 1, 2) == SCE_NGS2_OK && guard[0] == 0.0f && guard[1] == 0.0f); const Ngs2PanParam laterNan[2] = {front, nanAngle}; - float untouched[4] = {-1.0f, -1.0f, -1.0f, -1.0f}; - bool rejected = false; - try { sceNgs2PanGetVolumeMatrix(&work, laterNan, 2, 2, untouched); } catch (const std::invalid_argument&) { rejected = true; } - Require(rejected && std::all_of(std::begin(untouched), std::end(untouched), [](float level) { return level == -1.0f; })); + Require(panResult(&work, laterNan, 2, 2) == SCE_NGS2_OK); + Require(std::fabs(guard[0] - 0.707106769f) < 1e-6f && guard[1] == 0.0f && std::fabs(guard[2] - 0.707106769f) < 1e-6f && guard[3] == 0.0f); const Ngs2PanParam huge{1e30f, 1.0f, 1.0f, 0.0f}; Require(PanThrows(&work, &huge, 1, 2)); const float hugeSpeaker[] = {1e30f, 30.0f}; @@ -262,6 +281,9 @@ static void TestPan() { Require(sceNgs2PanInit(&work, nullptr, 360.0f, 2) == SCE_NGS2_OK); work.speaker_angles[0] = -INFINITY; Require(PanThrows(&work, &front, 1, 2)); + work.speaker_angles[0] = NAN; + const Ngs2PanParam right{30.0f, 1.0f, 1.0f, 0.0f}; + Require(panResult(&work, &right, 1, 2) == SCE_NGS2_OK && guard[0] == 0.0f && guard[1] == 0.0f); Require(sceNgs2PanInit(&work, nullptr, 360.0f, 2) == SCE_NGS2_OK); Ngs2PanWork marked; std::memset(&marked, 0x5a, sizeof(marked)); @@ -276,8 +298,14 @@ static void TestPan() { Require(sceNgs2PanInit(&marked, nullptr, 360.0f, count) == static_cast(0x804A8451)); Require(unchanged()); } - Require(PanInitThrows(&marked, nullptr, 360.0f, 0)); - Require(unchanged()); + const float sevenDegrees[] = {0.0f, 40.0f, 110.0f, 150.0f, 210.0f, 250.0f, 320.0f, 360.0f}; + for (const float unitAngle : {360.0f, 2.0f * pi, 180.0f, NAN}) { + Ngs2PanWork seven; + std::memset(&seven, 0x5a, sizeof(seven)); + Require(sceNgs2PanInit(&seven, nullptr, unitAngle, 0) == SCE_NGS2_OK && seven.num_speakers == 7); + Require(seven.unit_angle == (unitAngle == 2.0f * pi ? 2.0f * pi : 360.0f)); + for (std::uint32_t i = 0; i < 8; i++) Require(seven.speaker_angles[i] == sevenDegrees[i]); + } for (std::uint32_t count : {0u, 1u, 3u, 6u, 8u, 255u}) { Require(sceNgs2PanInit(&marked, pair, 360.0f, count) == static_cast(0x804A8451)); Require(sceNgs2PanInit(&marked, pair, 2.0f * pi, count) == static_cast(0x804A8451)); @@ -289,13 +317,31 @@ static void TestPan() { Require(unchanged()); } Require(sceNgs2PanInit(&work, pair, 2.0f * pi, 2) == SCE_NGS2_OK); - const float invalid[] = {0.0f, NAN}; - Require(PanInitThrows(&work, invalid, 360.0f, 2)); + const float nanSpeaker[] = {0.0f, NAN}; + Require(sceNgs2PanInit(&work, nanSpeaker, 360.0f, 2) == SCE_NGS2_OK && work.num_speakers == 2 && work.unit_angle == 360.0f); + Require(std::isnan(work.speaker_angles[0]) && work.speaker_angles[1] == 0.0f && std::isnan(work.speaker_angles[2])); const float onUnit[] = {720.0f, 30.0f}; const float belowZero[] = {-360.0f, 30.0f}; Require(PanInitThrows(&work, onUnit, 360.0f, 2) && !PanInitThrows(&work, belowZero, 360.0f, 2)); work.unit_angle = 100.0f; - Require(PanThrows(&work, &front, 1, 2)); + Require(panResult(&work, &front, 1, 2) == SCE_NGS2_ERROR_INVALID_PAN_UNIT_ANGLE); +} + +static void TestPanExplicitAnglesAndNanDistance() { + Ngs2PanWork work; + std::memset(&work, 0x5a, sizeof(work)); + Ngs2PanWork filled; + std::memcpy(&filled, &work, sizeof(work)); + const float pair[] = {-30.0f, 30.0f}; + Require(sceNgs2PanInit(&work, pair, 360.0f, 2) == SCE_NGS2_OK && work.num_speakers == 2 && work.unit_angle == 360.0f); + Require(work.speaker_angles[0] == 30.0f && work.speaker_angles[1] == 330.0f && work.speaker_angles[2] == 390.0f); + Require(std::memcmp(&work.speaker_angles[3], &filled.speaker_angles[3], 5 * sizeof(float)) == 0); + Require(sceNgs2PanInit(&work, nullptr, 360.0f, 2) == SCE_NGS2_OK); + for (std::uint32_t i = 3; i < 8; i++) Require(work.speaker_angles[i] == 0.0f); + const Ngs2PanParam params[2] = {{0.0f, 1.0f, 1.0f, 0.0f}, {30.0f, NAN, 1.0f, 0.0f}}; + float out[4] = {}; + Require(sceNgs2PanGetVolumeMatrix(&work, params, 2, 2, out) == SCE_NGS2_OK); + Require(std::fabs(out[0] - 0.707106769f) < 1e-6f && std::isnan(out[1]) && std::fabs(out[2] - 0.707106769f) < 1e-6f && std::isnan(out[3])); } static void TestPitchAndRepeat() { @@ -1585,6 +1631,7 @@ int main() { TestPcmBlockEnd(); TestPatchToRackHandle(); TestPan(); + TestPanExplicitAnglesAndNanDistance(); TestPitchAndRepeat(); TestSubmixerMatrix(); TestReverb(); diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index a49957e009..1a55fbed72 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -44,7 +44,7 @@ - [sceNgs2VoiceControl patch](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a destination rack handle with input id 0 resolves to that rack's first voice; this matches the Dead Cells call in #4334, but selection for racks with multiple voices has not been checked against a console. Nonzero destination input ids still throw. `sceNgs2VoiceGetPortInfo` reports the resolved voice handle and zero delay. - [sceNgs2ParseWaveformFile](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw - [sceNgs2VoiceControl sampler filter](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown -- [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. A speaker count of 0 without angles, which the firmware accepts, a non-finite speaker angle and a speaker angle equal to the unit angle, on which that library loops forever, throw. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason +- [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - inputs on which the PS5 library never returns throw: an infinite speaker or param angle, an angle too large for a turn to change it, a speaker angle that wraps to exactly the unit angle, and a param angle that wraps onto the first speaker plus one turn, such as one float below the first speaker in radians. Everything else matches the PS5 `libSceNgs2.native.sprx` of firmware 3.20 and 4.03 bit for bit, using its own `sinf`: 2, 4, 5 and 7 speakers, default and explicit angles in degrees and radians, matrix formats 1, 2, 6 and 8, distance and level sweeps and lists of 8 params. Speakers stay in the fixed input order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb, not sorted, and radian defaults keep the wrap-around entry in degrees, as on PS5 - [sceNgs2GeomResetListenerParam, sceNgs2GeomResetSourceParam](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) (libSceNgs2.native) - the structures (12, 16, 16, 60, 96, 108, 32 and 308 bytes) come from [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `ngs2.cpp` and match [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_geom.h` field for field, but only KytyPS5 gives the behaviour - shadPS4 logs and returns success without writing the output. The defaults (orient front +Z, orient up +Y, sound speed 343, cone 1/360/1/360, rolloff 0/1000000/1/1, doppler factor, full-band, LFE and maximum level 1, two speakers and matrix format 2) are unverified on a PS5 title; KytyPS5 and iStark/PS5PCEM agree on them. PPSA15552 (Dead Cells) calls `sceNgs2GeomResetSourceParam` after creating its racks. A null output address returns `INVALID_OUT_ADDRESS` as elsewhere in the library rather than throwing as KytyPS5 does. `sceNgs2GeomApply` is now a minimal pass-through that follows [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `Ngs2GeomApply` (shadPS4 only stubs it): pitch 1, source position, volume the larger of the source minimum and maximum level, and a diagonal of the source maximum level (1 when it is not positive) over `matrix_format` channels (0 is two, at most 8). The listener and `flags` are ignored, so source attenuation, distance rolloff, panning and Doppler stay unimplemented; neither KytyPS5 nor this has been measured on a PS5, and a null output address returns `INVALID_OUT_ADDRESS` where KytyPS5 exits - [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) builds an orthonormal world-to-listener transform. The row-major matrix, front-axis priority and flag 1 for right-handed input follow [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/src/hle/ngs2_geom.zig); they have not been measured on PS5 hardware. Velocity stays in world coordinates and positive finite sound speed is preserved. Orientation vectors are normalised and up is made perpendicular to front, so non-unit and slightly tilted vectors are accepted. When front and up are parallel, up is replaced by `(0,1,0)` or `(1,0,0)` so the basis stays defined: PPSA15552 (Dead Cells) passes `orient_front={0,0,1}` with `orient_up={0,0,1}` after moving the listener, which PS5PCEM rejects; the console error code is unknown. Zero orientation vectors, nonfinite input and an unrepresentable translation still throw without changing the output; console error codes are unknown, and a nonpositive sound speed throws where PS5PCEM and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/ngs2.cpp) default it to 343. KytyPS5 always writes the identity matrix, so the transform rests on PS5PCEM plus that Dead Cells case. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334. This does not implement source attenuation, panning or Doppler in `sceNgs2GeomApply`. - [NGS2 custom sampler rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4001) - implements bounded support for the custom sampler rack and its built-in module 0x13 (Custom Voice Filter), unblocking PPSA15552 (Dead Cells) startup. Structure layouts come from [shadPS4's ngs2_custom.h](https://raw.githubusercontent.com/shadps4-emu/shadPS4/main/src/core/libraries/ngs2/ngs2_custom.h) (OrbisNgs2CustomVoiceFilterParam, 40 bytes) and live trace correlation from [prosper's hle_audio.cpp](https://raw.githubusercontent.com/mattias800/prosper/main/prosper/src/hle/audio/hle_audio.cpp) (0x40001300 command and parameters like 10000.0f and 0.707f). Routes parameter updates via the 0x40001300 command family into the Biquad filter engine. Filter type 0 clears the destination filter; type 1 configures the RBJ low-pass biquad frequency, Q and level. Other filter types and unmodeled configurations throw. From 048bb887a7867850dcf3331767d225d095e99f6c Mon Sep 17 00:00:00 2001 From: Himansh Raj Date: Sat, 24 Oct 2026 04:34:11 +0530 Subject: [PATCH 155/428] build: cast away const before the error_category vtable call on macOS Apple clang rejects reinterpret_cast from const void* to a function pointer ("casts away qualifiers") in StandardExceptions.cpp, so libc/src/exception/Exports.cpp, which includes it, failed to build on macOS after std::system_category landed. On macOS the vtable slot is const_cast first. Linux and Windows compile the same code as before. --- core/libs/prx/libc/src/exception/StandardExceptions.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/prx/libc/src/exception/StandardExceptions.cpp b/core/libs/prx/libc/src/exception/StandardExceptions.cpp index bf6b539f82..9e19717097 100644 --- a/core/libs/prx/libc/src/exception/StandardExceptions.cpp +++ b/core/libs/prx/libc/src/exception/StandardExceptions.cpp @@ -241,7 +241,11 @@ static GuestLocale::String* APS5_VABI CategoryMessage_nid_no_patch(GuestLocale:: static ErrorCode APS5_VABI CategoryCondition_nid_no_patch(const CategoryObject* self, int value) { return {0, value, self}; } static bool APS5_VABI CategoryEquivalentCondition_nid_no_patch(const CategoryObject* self, int value, const ErrorCode* condition) { using Condition = ErrorCode (APS5_VABI *)(const CategoryObject*, int); +#ifdef __APPLE__ + const auto mapped = reinterpret_cast(const_cast(static_cast(self->vtable)[4]))(self, value); +#else const auto mapped = reinterpret_cast(static_cast(self->vtable)[4])(self, value); +#endif return condition->category == mapped.category && condition->value == mapped.value; } static bool APS5_VABI CategoryEquivalentCode_nid_no_patch(const CategoryObject* self, const ErrorCode* code, int value) { From 68db53d7a597dab5624de8ebdc68bf653a969258 Mon Sep 17 00:00:00 2001 From: Retr0 Date: Sat, 24 Oct 2026 02:44:14 +0300 Subject: [PATCH 156/428] fix(libkernel): fail sendfile with EINVAL for a file that is not a regular file --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 12 +++++++----- core/libs/tests/GuestSendfile.cpp | 12 ++++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 078c6d9aec..dcd5bdda3d 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -38,6 +38,7 @@ #include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" +#include "prx/libkernel/File/include/SharedMemory.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" #include "prx/libkernel/Socket/include/SocketConnect.hpp" @@ -1665,6 +1666,12 @@ int APS5_VABI sendfile_nid_postfix(int file, int descriptor, std::int64_t offset if (offset < 0) return Fail(22); if (headers && (!SendfileIovecs(headers->headers, headers->headerCount) || !SendfileIovecs(headers->trailers, headers->trailerCount))) return Fail(22); + std::int64_t sent = 0; + const auto finish = [&](int error) { + if (sbytes) *sbytes = sent; + return error ? Fail(error) : 0; + }; + if (DescriptorIsOpen_nid_no_patch(file) && !File::IsVnode(file) && !File::IsSharedMemoryDescriptor_nid_no_patch(file)) return finish(22); std::vector chunk; try { chunk.resize(0x10000); @@ -1678,11 +1685,6 @@ int APS5_VABI sendfile_nid_postfix(int file, int descriptor, std::int64_t offset std::uint64_t remaining = length ? length : UINT64_MAX; auto received = read(offset, remaining); if (received == -9) return Fail(9); - std::int64_t sent = 0; - const auto finish = [&](int error) { - if (sbytes) *sbytes = sent; - return error ? Fail(error) : 0; - }; if (received < 0) return finish(received == -21 || received == -29 ? 22 : static_cast(-received)); const auto socket = Lookup(descriptor); if (!socket) return finish(*__error_nid_postfix()); diff --git a/core/libs/tests/GuestSendfile.cpp b/core/libs/tests/GuestSendfile.cpp index 9759531b34..c38a4f95b8 100644 --- a/core/libs/tests/GuestSendfile.cpp +++ b/core/libs/tests/GuestSendfile.cpp @@ -22,6 +22,7 @@ int APS5_VABI close_nid_postfix(int); std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int); int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI socketpair_nid_postfix(int, int, int, int*); +int APS5_VABI kqueue_nid_postfix(); std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); int APS5_VABI ioctl_nid_postfix(int, std::uint64_t, void*); int APS5_VABI sendfile_nid_postfix(int, int, std::int64_t, std::uint64_t, const Hdtr*, std::int64_t*, int); @@ -98,6 +99,17 @@ int main() { Require(unconnected >= 0); sent = 77; Require(Fails(sendfile_nid_postfix(file, unconnected, size + 10, 0, nullptr, &sent, 0), 57) && sent == 0); + const int random = open_nid_postfix("/dev/urandom", 0, 0); + Require(random >= 0); + sent = 77; + Require(Fails(sendfile_nid_postfix(random, pair[0], 0, 16, &parts, &sent, 0), 22) && sent == 0); + sent = 77; + Require(Fails(sendfile_nid_postfix(random, pair[0], 0, 0, nullptr, &sent, 0), 22) && sent == 0); + const int queue = kqueue_nid_postfix(); + Require(queue >= 0); + sent = 77; + Require(Fails(sendfile_nid_postfix(queue, pair[0], 0, 0, nullptr, &sent, 0), 22) && sent == 0); + Require(close_nid_postfix(queue) == 0 && close_nid_postfix(random) == 0); const std::string large = Content(32 << 20); const int largeFile = OpenWritten("sendfile_large.bin", large); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 80453ebf50..8ed4ac5855 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -236,7 +236,7 @@ - [_accept, _bind, _connect, _getpeername, _getsockname, _getsockopt, _listen, _recvfrom, _recvmsg, _sendmsg, _sendto, _setsockopt](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp), [_getdirentries](../../../core/libs/prx/libkernel/File/src/Stdio.cpp), [_sigaction, _sigsuspend](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) are not measured on the console. They forward to the calls without the underscore, which have no libthr layer here, as FreeBSD 11.4 makes the underscore names the plain system calls. `_sigaction` and `sigaction` share one handler table, where FreeBSD's `__thr_sigaction` hands its own wrapper to the system call `_sigaction` reads back, and rejects `SIGCANCEL` (32). A null set throws in `_sigsuspend` as in `sigsuspend`, while the system call FreeBSD 11.4 exports as `_sigsuspend` ([`Symbol.map`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/sys/Symbol.map#L961)) returns `EFAULT` - [Regular file mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - on Linux `mmap` maps a regular file's descriptor shared or private like a shared memory object, with the same access checks; SQLite's WAL index (`-shm`) in Dusklight PPSA99305 needs it. FreeBSD 11 `mmap(2)` and shadPS4's `MemoryManager::MapFile` map regular files; the console's behaviour is not measured. Other descriptors (pipes, sockets, directories, devices) still fail with `EOPNOTSUPP`. On Windows a shared mapping is a view of a file section (`CreateFileMappingW` on the descriptor's handle, read-write when the descriptor is, otherwise read-only), so writes through the descriptor and the mapping see each other. This also maps anonymous `shm_open` objects and JIT shared memory descriptors, which are temporary files on Windows. A shared mapping may end inside the 16 KiB page that holds the end of the file, as POSIX [`mmap`](https://pubs.opengroup.org/onlinepubs/9799919799/functions/mmap.html) allows (llama.cpp in PS5LM Plus PPSA99581 maps a whole model file this way): the section keeps the file's size, so the file never grows, the view ends with the 4 KiB host page that holds the end of the file, which Windows fills with zeros past it, and the rest of the guest page is private zero pages. As POSIX requires, the bytes past the end read zero and are never written to the file; bytes written to those private pages are seen only through that mapping, not through other mappings of the file, and a file that grows later does not show its new bytes there. A shared mapping that reaches a whole page past the end of the file fails with `EOPNOTSUPP`, where POSIX maps it and delivers `SIGBUS` on access, as do `PROT_EXEC` and a write-only descriptor. A read-write alias (`GuestArenaMapAlias`) of a mapped page that holds the end of the file throws. `mprotect` above the view's protection (writing through a read-only descriptor, or adding `PROT_EXEC`) fails with `EACCES` before anything changes; FreeBSD allows `PROT_EXEC` on a readable descriptor. Shrinking the file with `ftruncate` fails with `EINVAL` while any shared mapping of it exists, even one that does not cover the removed part, because the section keeps the file's size at `mmap`; FreeBSD truncates. A read-write alias of a view (`GuestArenaMapAlias`, used when the GPU imports guest memory) of a read-only file section fails, as the alias is mapped `PAGE_READWRITE`. Views of the same file made by separate `mmap` calls are separate sections, so write tracking does not treat them as aliases of each other. A private mapping on Windows is a copy of the file read at `mmap`, which POSIX allows (later changes to the file need not show); pages wholly past the end of the file fail with `EOPNOTSUPP` instead of faulting on access, and a write-only descriptor fails with `EOPNOTSUPP`. - [File mappings on macOS](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `mmap` of a regular file, of an anonymous `shm_open` object (an unlinked temporary file on macOS) or of a JIT shared memory descriptor maps the descriptor shared or private as on Linux, with the same access checks. macOS refuses to create an executable file mapping (`EPERM`), so an executable mapping is made without execute and then given it with `mprotect`. Under Rosetta 2 (Apple M2, macOS 26), code rewritten through one view of a file after it ran from another view keeps its old translation until the executable view's protection changes. Named `shm_open` objects still throw on macOS. -- [sendfile](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) follows FreeBSD 11's `sendfile` for a connected stream socket: headers, the file from `offset` (to its end when `nbytes` is 0) and trailers, the file offset left unchanged, `EAGAIN` with `*sbytes` set after a partial send on a non-blocking socket. It reads the file through `sceKernelPread` in 64 KiB blocks and sends each with `send`. A socket as the file fails with `EINVAL`, as on FreeBSD, but any other descriptor `sceKernelPread` reads, such as `/dev/urandom`, is sent where FreeBSD requires a regular file or shared memory. `flags` are ignored: reads complete before sending, so `SF_NODISKIO` never fails with `EBUSY`, and `SF_SYNC` returns once the data is queued on the socket rather than sent. PS5MC (PPSA99420) imports it +- [sendfile](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) follows FreeBSD 11's `sendfile` for a connected stream socket: headers, the file from `offset` (to its end when `nbytes` is 0) and trailers, the file offset left unchanged, `EAGAIN` with `*sbytes` set after a partial send on a non-blocking socket. It reads the file through `sceKernelPread` in 64 KiB blocks and sends each with `send`. A file that is not a regular file or shared memory, such as a socket, `/dev/urandom` or a kqueue, fails with `EINVAL` and `*sbytes` 0, as FreeBSD's [`sendfile_getobj`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_sendfile.c#L421-L463) refuses it. `flags` are ignored: reads complete before sending, so `SF_NODISKIO` never fails with `EBUSY`, and `SF_SYNC` returns once the data is queued on the socket rather than sent. PS5MC (PPSA99420) imports it - [pthread_kill](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread is left on the pending set for whichever thread waits for or unblocks it, since it is not delivered to the chosen thread, and a handle that is not a live guest thread returns `ESRCH` without touching it - [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) validates `tv_usec` but saturates a valid timeout beyond the remaining `steady_clock` range at `time_point::max()`, so the wait can continue effectively indefinitely. The console's behavior for a valid timeout outside the host clock's range is unknown. - [inet_ntop, inet_pton](../../../core/libs/prx/libkernel/Socket/src/AddressConversion.cpp) (libkernel) - an `AF_INET6` result of `inet_ntop` is followed by zero bytes up to `size`. In a payload process on a PS5 (firmware 12.70), the text forms, the order of the checks and this padding were measured through both exports. The padding was sampled at sizes from 3 to 65536, above 64 only at 65, 128, 256, 1024, 4096 and 65536; the sizes that were not sampled, sizes above 65536 and a title process are assumed to behave the same From 44141f9aafc0d1a5bf20a764fdf69f0746112131 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 22 Oct 2026 16:35:44 +0000 Subject: [PATCH 157/428] fix(agc): settle a pending import synchronously before an initial multisampled DCC import Rush 2 Recompiled (PPSA69423) and Wave Race 64 Recompiled (PPSA69424) stop on Windows with "initial multisampled DCC contents require watched guest memory" (https://github.com/boykopovar/AnyPS5/issues/9724). Their multisampled color target lives in a 784 KiB host import made at map time. An import under 1 MiB stays compared until a later lookup reaps its deferred settle read, and the titles draw to the target first. When an initial multisampled DCC import needs a range whose import still waits for its settle read, submit that read now if it is still wanted, wait for its fence and return the import to the write watch before the check. This builds on #8527's ordering: the read goes through its main-queue batch with barriers around it, so it runs after every earlier main-queue access, and the wait happens under the GPU mutex that submission holds. An import that is not a whole number of 64 KiB blocks, such as these titles' 0xc4000 bytes, never got a deferred read, and Rewatch refuses a range that does not start and end on a block. Such an import now waits for this path alone. It returns to the write watch with its partial blocks whole, each stamped as a CPU store, unless another compared import or a live storage image shares them. Imports that stay compared for any other reason still do, an explicitly unwatched range is still refused, and APS5_NO_DEFERRED_SETTLES still keeps these imports compared. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +- .../Graphics/include/GuestBufferMemory.hpp | 2 + .../Graphics/src/GuestBufferMemory.cpp | 91 ++++++++- .../Graphics/src/MultisampleTarget.cpp | 3 +- .../prx/libSceAgcDriver/tests/Recorder.cpp | 184 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 2 +- 6 files changed, 277 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 3b1a03da36..3506da9069 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1538,6 +1538,8 @@ if(BUILD_TESTING) endif() add_test(NAME agc_import_watch COMMAND agc_driver_recorder_tests --import-watch-only) set_tests_properties(agc_import_watch PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_import_settle_initial_dcc COMMAND agc_driver_recorder_tests --settle-initial-dcc-only) + set_tests_properties(agc_import_settle_initial_dcc PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) if(WIN32) add_test(NAME agc_import_watch_other_family COMMAND agc_driver_recorder_tests --import-watch-other-family-only) set_tests_properties(agc_import_watch_other_family PROPERTIES SKIP_RETURN_CODE 77) @@ -1555,7 +1557,7 @@ if(BUILD_TESTING) add_test(NAME agc_import_settle_retire COMMAND agc_driver_recorder_tests --settle-retire-only) add_test(NAME agc_import_settle_storage_image COMMAND agc_driver_recorder_tests --settle-storage-image-only) set_tests_properties(agc_import_settle_deferred agc_import_settle_deferred_other_family agc_import_settle_first_use agc_import_settle_earlier_import agc_import_settle_retire agc_import_settle_storage_image PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) - set_tests_properties(agc_import_watch agc_import_watch_other_family agc_import_settle_deferred agc_import_settle_deferred_other_family agc_import_settle_first_use agc_import_settle_earlier_import agc_import_settle_retire agc_import_settle_storage_image PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_import_watch agc_import_watch_other_family agc_import_settle_deferred agc_import_settle_deferred_other_family agc_import_settle_first_use agc_import_settle_earlier_import agc_import_settle_retire agc_import_settle_storage_image agc_import_settle_initial_dcc PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index 054c34be29..97e12def1d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -38,6 +38,7 @@ struct HostImport { // Identity for the life of this import (see HostImportSerial); 0 until first asked for. std::uint64_t serial = 0; bool unwatched = false; + bool settleOnDemand = false; bool dmaBuf = false; bool joined = false; std::shared_ptr chunk; @@ -102,6 +103,7 @@ void ReportHostImports(); // address)` in the import's buffer. const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes, ImportRequester requester = ImportRequester::Access); const HostImport* ExistingHostImportFor(const Context& context, std::uint64_t address, std::size_t bytes); +bool WatchedAfterSettle(const Context& context, std::uint64_t address, std::size_t bytes); void SetHostImportWindow(std::uint64_t bytes); // Tests: the window of one large registered range (64 MiB) and the import budget (0: the configured one). void SetHostImportRangeWindow(std::uint64_t bytes); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index e72a4239ed..e8974f1e34 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1094,8 +1094,22 @@ void reapSettleBatches(const Context& context, HostImports& state) { } } +bool comparedImportShares(const HostImports& state, std::uint64_t own, std::uint64_t begin, std::uint64_t end) { + const auto shares = [&](const HostImport& entry) { return entry.unwatched && entry.memoryBase < end && begin < entry.memoryBase + entry.pinnedBytes; }; + for (const auto& [base, entry] : state.imports) { + if (base != own && shares(entry)) return true; + } + for (const auto& [device, list] : state.parked) { + for (const auto& retired : list) { + if (shares(retired->node.mapped())) return true; + } + } + return false; +} + void rewatchSettledImports(HostImports& state) { if (state.settleReady.empty()) return; + std::vector bases; std::vector> ranges; for (const auto base : state.settleReady) { const auto settle = state.pendingSettles.find(base); @@ -1103,19 +1117,23 @@ void rewatchSettledImports(HostImports& state) { state.pendingSettles.erase(settle); const auto import = state.imports.find(base); if (import == state.imports.end() || !import->second.unwatched) continue; - const std::array, 1> span{{{base, base + import->second.bytes}}}; + const auto first = base & ~(settleBlockBytes - 1); + const auto stop = (base + import->second.bytes + settleBlockBytes - 1) & ~(settleBlockBytes - 1); + if ((first != base || stop != base + import->second.bytes) && comparedImportShares(state, base, first, stop)) continue; + const std::array, 1> span{{{first, stop}}}; if (StorageTexture::AnyLiveOverlaps(span)) { ++state.settlesKeptCompared; continue; } + bases.push_back(base); ranges.push_back(span.front()); } state.settleReady.clear(); if (ranges.empty()) return; GuestMemory::Rewatch(ranges); - for (const auto& [begin, end] : ranges) { - const auto import = state.imports.find(begin); - if (import == state.imports.end() || !GuestMemory::Watched(begin, static_cast(end - begin))) continue; + for (const auto base : bases) { + const auto import = state.imports.find(base); + if (import == state.imports.end() || !GuestMemory::Watched(base, static_cast(import->second.bytes))) continue; import->second.unwatched = false; ++state.settlesRewatched; } @@ -1130,6 +1148,49 @@ void settleUnwatchedImports(const Context& context, HostImports& state) { rewatchSettledImports(state); submitSettleBatch(context, state); } + +void settleImportsNow(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end) { + if (context.settleQueue == VK_NULL_HANDLE || context.settleQueueMutex == nullptr || !GuestMemory::GpuMutex().HeldByThisThread()) return; + std::vector needed; + auto it = state.imports.lower_bound(begin); + if (it != state.imports.begin()) --it; + for (; it != state.imports.end() && it->first < end; ++it) { + auto& entry = it->second; + if (entry.base + entry.bytes <= begin || !entry.unwatched) continue; + auto settle = state.pendingSettles.find(it->first); + if (settle == state.pendingSettles.end()) { + if (!entry.settleOnDemand || !deferredSettles.load(std::memory_order_relaxed)) continue; + entry.settleOnDemand = false; + settle = state.pendingSettles.insert_or_assign(it->first, PendingSettle{}).first; + } + if (settle->second.phase == SettlePhase::Wanted) { + std::erase(state.settleWanted, it->first); + state.settleWanted.insert(state.settleWanted.begin(), it->first); + } + needed.push_back(it->first); + } + if (needed.empty()) return; + auto& deviceSettle = settleFor(context, state); + const auto wanted = [&] { + return std::any_of(needed.begin(), needed.end(), [&](std::uint64_t base) { + const auto settle = state.pendingSettles.find(base); + return settle != state.pendingSettles.end() && settle->second.phase == SettlePhase::Wanted; + }); + }; + if (wanted()) { + if (std::all_of(deviceSettle.batches.begin(), deviceSettle.batches.end(), [](const SettleBatch& batch) { return batch.busy; })) { + for (const auto& batch : deviceSettle.batches) waitSettleBatch(deviceSettle, context.device, batch); + reapSettleBatches(context, state); + } + submitSettleBatch(context, state); + } + for (const auto base : needed) { + const auto settle = state.pendingSettles.find(base); + if (settle != state.pendingSettles.end() && settle->second.phase == SettlePhase::Submitted && settle->second.device == context.device) waitSettleBatch(deviceSettle, context.device, deviceSettle.batches[settle->second.batch]); + } + reapSettleBatches(context, state); + rewatchSettledImports(state); +} #endif void decideImportWatch(const Context& context, HostImports& state) { @@ -1903,8 +1964,10 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import entry.joined = joined; entry.lastUse = ++state.useCounter; #ifdef _WIN32 - if (deferredSettles.load(std::memory_order_relaxed) && entry.alias == nullptr && state.watchPrivateImports && writable && base % settleBlockBytes == 0 && bytes % settleBlockBytes == 0) { - if (entry.unwatched) { + if (deferredSettles.load(std::memory_order_relaxed) && entry.alias == nullptr && state.watchPrivateImports && writable) { + if (base % settleBlockBytes != 0 || bytes % settleBlockBytes != 0) { + entry.settleOnDemand = entry.unwatched; + } else if (entry.unwatched) { state.pendingSettles.insert_or_assign(base, PendingSettle{}); state.settleWanted.push_back(base); } else if (settled && !GuestMemory::Watched(base, static_cast(bytes))) { @@ -3657,6 +3720,22 @@ const HostImport* ExistingHostImportFor(const Context& context, std::uint64_t ad return findImport(state, address, address + bytes); } +bool WatchedAfterSettle(const Context& context, std::uint64_t address, std::size_t bytes) { +#ifdef _WIN32 + if (context.hostImportAlignment != 0 && bytes != 0 && bytes <= std::numeric_limits::max() - address && GuestMemory::GpuMutex().HeldByThisThread()) { + ensurePinWaiter(); + auto& state = Imports(); + std::lock_guard lock(state.mutex); + if (importsStale(context, state)) { + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + refreshImports(context, state, lease); + } + settleImportsNow(context, state, address, address + bytes); + } +#endif + return GuestMemory::Watched(address, bytes); +} + bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice) { if (context.hostImportAlignment == 0) return true; ensurePinWaiter(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 7a9856eff4..90f04b9c1f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/FirstSampleCopy.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" @@ -574,7 +575,7 @@ private: if (use.cmaskAddress != 0) dccRanges.push_back({use.cmaskAddress, use.cmaskBytes, 0}); for (auto& range : dccRanges) { GuestMemory::CheckRange(reinterpret_cast(range.address), range.bytes, 256); - Require(GuestMemory::Watched(range.address, range.bytes), "initial multisampled DCC contents require watched guest memory"); + Require(WatchedAfterSettle(context, range.address, range.bytes), "initial multisampled DCC contents require watched guest memory"); range.generation = collectBlocks(range.address, range.bytes); Require(range.generation != 0, "initial multisampled DCC contents have no write generation"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 9e6af9dfd5..a18bfd9ee2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2518,6 +2518,182 @@ bool settleStorageImageTests(const Device& device) { std::cout << "a settled import under a storage image tests passed\n"; return true; } + +bool settleInitialDccTests(const Device& device) { + using namespace AgcDriver::GuestMemory; + const auto& base = device.GetContext(); + constexpr std::size_t unit = 65536; + constexpr std::size_t tail = 0x4000; + if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0 || !device.Graphics() || base.hostImportAlignment == 0 || base.hostImportAlignment > tail || base.settleQueue == VK_NULL_HANDLE || !WriteWatched()) { + std::cout << "4x color targets, graphics, host imports within 16 KiB, a settle queue or write watching unavailable: an initial multisampled DCC import over a pending settle not tested\n"; + return false; + } + { + std::lock_guard gpu(GpuMutex()); + PrepareImportWatch(base); + if (!settledImportsWatchPrivate(base)) { + std::cout << "this device compares every Windows import: an initial multisampled DCC import over a pending settle not tested\n"; + return false; + } + } + std::cout << "write watch: Windows private imports stay watched after a settle read; imports under 1 MiB wait for a deferred settle\n"; + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + Recorder recorder(context); + recorder.Activate(); + enum class Case { Pending, PartialBlock, ComparedNeighbour, DeferredOff, UnwatchedKeys }; + const auto run = [&](Case kind, std::uint32_t height) { + const bool partial = kind == Case::PartialBlock || kind == Case::ComparedNeighbour; + ColorTarget target{}; + target.extent = {128, height}; + target.surfaceExtent = target.extent; + target.format = VK_FORMAT_R8G8B8A8_UNORM; + target.elementBytes = 4; + target.samples = 4; + target.tileMode = ColorTileMode::RenderTarget; + target.dccPipeAligned = true; + const auto colorBytes = static_cast(MultisampledColorBytes(target)); + const auto dccBytes = MultisampledDccBytes(target); + const auto importBytes = colorBytes + (partial ? tail : unit); + const auto reserved = (importBytes + unit - 1) & ~(unit - 1); + const auto regionBytes = unit + reserved + unit + (kind == Case::UnwatchedKeys ? unit : 0); + auto* region = static_cast(AllocateWatched(regionBytes, unit)); + void* block = region != nullptr ? region + unit : nullptr; + void* keys = region != nullptr && kind == Case::UnwatchedKeys ? region + unit + reserved + unit : nullptr; + const auto destBytes = static_cast(target.extent.width) * target.extent.height * 4u; + void* dest = VirtualAlloc(nullptr, destBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + Require(block != nullptr && dest != nullptr && (kind != Case::UnwatchedKeys || keys != nullptr), "(m) cannot allocate the target"); + const auto address = reinterpret_cast(block); + const auto neighbour = address + importBytes + tail; + const auto describe = [](const HostImport* found, std::uint64_t at, std::size_t bytes) { + char text[224]; + if (found == nullptr) std::snprintf(text, sizeof(text), ": no import of 0x%llx+0x%zx, watched %d", static_cast(at), bytes, Watched(at, bytes) ? 1 : 0); + else std::snprintf(text, sizeof(text), ": import 0x%llx+0x%llx, unwatched %d, for 0x%llx+0x%zx, watched %d", static_cast(found->base), static_cast(found->bytes), found->unwatched ? 1 : 0, static_cast(at), bytes, Watched(at, bytes) ? 1 : 0); + return std::string(text); + }; + struct Release { + const Context& context; + Recorder& recorder; + void* region; + std::size_t regionBytes; + void* block; + void* keys; + void* dest; + std::size_t reserved; + std::uint64_t neighbour; + ~Release() { + { + std::lock_guard gpu(GpuMutex()); + recorder.Sync(); + ClearMultisampleTargets(context.device); + ClearCachedTextures(context.device); + recorder.FlushDeferredReleases(); + } + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + mutation.Remove(dest); + if (keys != nullptr) mutation.Remove(keys); + if (neighbour < reinterpret_cast(block) + reserved) mutation.Remove(reinterpret_cast(neighbour)); + } + HostImportFor(context, reinterpret_cast(block), 16); + SetHostImportDeferredSettles(true); + ReleaseWatched(region, regionBytes); + VirtualFree(dest, 0, MEM_RELEASE); + } + } release{context, recorder, region, regionBytes, block, keys, dest, reserved, kind == Case::ComparedNeighbour ? neighbour : std::numeric_limits::max()}; + target.address = address; + target.surfaceAddress = address; + target.bytes = colorBytes; + target.dccAddress = kind == Case::UnwatchedKeys ? reinterpret_cast(keys) : address + colorBytes; + std::memset(block, 0, importBytes); + std::memset(reinterpret_cast(target.dccAddress), 0xc0, dccBytes); + std::memset(dest, 0x5a, destBytes); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, importBytes, true, true, true); + mutation.Add(dest, destBytes, true, true, true); + if (keys != nullptr) mutation.Add(keys, unit, true, true, true); + if (kind == Case::ComparedNeighbour) mutation.Add(reinterpret_cast(neighbour), reserved - (importBytes + tail), true, true, true); + } + if (kind == Case::UnwatchedKeys) Unwatch(target.dccAddress, dccBytes); + if (kind == Case::DeferredOff) SetHostImportDeferredSettles(false); + if (kind == Case::ComparedNeighbour) { + const auto* other = HostImportFor(context, neighbour, reserved - (importBytes + tail)); + Require(other != nullptr && other->unwatched && !Watched(neighbour, reserved - (importBytes + tail)), "(m) the import sharing the target's last 64 KiB block was not compared" + describe(other, neighbour, reserved - (importBytes + tail))); + } + const auto* import = HostImportFor(context, address, importBytes); + Require(import != nullptr && import->base == address && import->bytes == importBytes && import->unwatched && !Watched(address, importBytes), "(m) the target's import of " + std::to_string(importBytes) + " bytes was not compared at first" + describe(import, address, importBytes)); + std::unique_lock gpu(GpuMutex()); + if (partial) { + for (int round = 0; round < 20; ++round) { + gpu.unlock(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + gpu.lock(); + HostImportFor(context, address, importBytes); + } + Require(!Watched(address, importBytes), "(m) a deferred settle returned an import that is not a whole number of 64 KiB blocks to the write watch"); + } + const auto before = HostImportSettles(); + std::string refusal; + try { + Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "(m) the multisampled DCC target has no view"); + } catch (const std::exception& error) { + refusal = error.what(); + } + const auto after = HostImportSettles(); + const auto* settled = ExistingHostImportFor(context, address, importBytes); + const char* name = kind == Case::Pending ? "a pending settle" : kind == Case::PartialBlock ? "an import of whole and partial 64 KiB blocks" : kind == Case::ComparedNeighbour ? "a compared import in its last 64 KiB block" : kind == Case::DeferredOff ? "deferred settles off" : "explicitly unwatched DCC keys"; + std::cout << "(m) initial DCC over " << name << " (import " << importBytes << " bytes): " << (refusal.empty() ? "accepted" : refusal) << "; " << after.submitted - before.submitted << " settle reads submitted, " << after.rewatched - before.rewatched << " back in the write watch\n"; + if (kind == Case::ComparedNeighbour || kind == Case::DeferredOff || kind == Case::UnwatchedKeys) { + Require(refusal.find("initial multisampled DCC contents require watched guest memory") != std::string::npos, std::string("(m) an initial multisampled DCC import over ") + name + " was not refused: " + refusal); + Require(kind == Case::UnwatchedKeys || !Watched(address, importBytes), std::string("(m) ") + name + " returned the target's import to the write watch"); + Require(kind != Case::UnwatchedKeys || (Watched(address, importBytes) && !Watched(target.dccAddress, dccBytes)), "(m) the refusal over explicitly unwatched DCC keys did not come from the keys" + describe(settled, address, importBytes)); + return; + } + Require(refusal.empty(), std::string("(m) an initial multisampled DCC import over ") + name + " was refused: " + refusal); + Require(after.submitted > before.submitted && after.rewatched > before.rewatched && Watched(address, importBytes) && settled != nullptr && !settled->unwatched, std::string("(m) the initial DCC import over ") + name + " did not settle the import and return it to the write watch"); + ColorTarget destination{}; + destination.address = reinterpret_cast(dest); + destination.surfaceAddress = destination.address; + destination.extent = target.extent; + destination.surfaceExtent = target.extent; + destination.format = VK_FORMAT_R8G8B8A8_UNORM; + destination.elementBytes = 4; + destination.bytes = destBytes; + destination.samples = 1; + destination.tileMode = ColorTileMode::RenderTarget; + RunColorMetadataPass(context, {ColorMetadataPass::Mode::Resolve, {destination}, target}); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + std::vector resolved(destBytes / 4); + AgcDriver::GuestMemory::Read(destination.address, std::as_writable_bytes(std::span(resolved))); + Require(std::ranges::all_of(resolved, [](std::uint32_t word) { return word == 0xffffffffu; }), std::string("(m) the target made over ") + name + " did not start from its DCC fast clear"); + BumpCollectEpoch(); + const auto collected = CollectWrites(address, importBytes); + Require(collected != 0, "(m) the settled import is not collected"); + static_cast(block)[importBytes - 1] = 0x77; + static_cast(block)[colorBytes] = 0x00; + CollectWritesUncached(address, importBytes); + Require(!UnchangedSince(address + importBytes - 1, 1, collected) && !UnchangedSince(address + colorBytes, 1, collected), "(m) a CPU store after the initial DCC import was missed"); + std::string reuse; + try { + MultisampleTargetView(context, target); + } catch (const std::exception& error) { + reuse = error.what(); + } + Require(reuse.find("writes to a resident multisampled DCC target or its metadata are not implemented") != std::string::npos, "(m) the resident target missed the CPU store to its DCC keys: " + reuse); + }; + run(Case::Pending, 128); + run(Case::PartialBlock, 384); + run(Case::ComparedNeighbour, 384); + run(Case::DeferredOff, 128); + run(Case::UnwatchedKeys, 128); + std::cout << "initial multisampled DCC over a pending settle tests passed\n"; + return true; +} #endif void remappedImportTests(const Device& device) { @@ -10828,6 +11004,14 @@ int main(int argc, char** argv) { importWatchTests(device); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--settle-initial-dcc-only") { +#ifdef _WIN32 + return settleInitialDccTests(device) ? 0 : 77; +#else + std::cout << "skipped, the device has no deferred settle of host imports: imports under 1 MiB wait for one only on Windows\n"; + return 77; +#endif + } #ifdef _WIN32 if (argc == 2 && std::string_view(argv[1]) == "--host-import-lifetime-other-family-only") { if (device.GetContext().settleFamily == device.GetContext().queueFamily) { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index e95cdc997f..2aacf360dc 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -118,7 +118,7 @@ - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending saves its bytes through the unwatch hook, which also runs when a failed collect takes a range out of the watch. A range that leaves the watch without the hook running over it (an import fallback that unwatched without saving) leaves its pending results without saved bytes; storing such results throws. A unit whose results were stored while its blocks were watched keeps only a generation; when its blocks leave the watch while nothing is pending, the hook saves nothing, and results that land in it afterwards have neither saved bytes nor a watch, so storing them throws too. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory when the import probe keeps the write watch and a settle queue exists: those of 1 MiB or more stay watched, and smaller ones return to the watch once their settle has completed, unless a storage image overlaps them (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. - [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) whose protection an import resolves count as stored only when their bytes change. A same-value CPU store during that window cannot be distinguished from driver activity and does not win over pending results. On Windows, NVIDIA 617.42 marks every private MEM_WRITE_WATCH page after import until a GPU use completes. The driver behavior was measured on this driver; other drivers are unverified. Imports of at least 1 MiB wait for a four-byte read on the separate settle queue inside the window. Smaller imports stay compared until a deferred read completes; a later lookup returns whole blocks to write watching with a new CPU-store stamp. Blocks overlapping a live storage image, blocks of an import retired before rewatching, and imports without a settle queue stay compared. Shared-memory imports retain their separate-alias tracking. -- [Deferred host-import reads](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) use the main queue with barriers ordering earlier and later writes around the reads. Submission does not wait on the CPU. The latency cost of placing batches behind main-queue work is unmeasured. The first-use test now checks that earlier main-queue reads finish before the import returns to write watching; the original two-queue canary measured driver marking while those reads were still in flight. The earlier-import canary still checks later use of another import of the same pages. +- [Deferred host-import reads](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) use the main queue with barriers ordering earlier and later writes around the reads. Submission does not wait on the CPU. An initial multisampled DCC import needing a range whose import still awaits its read submits it if needed and waits under the GPU mutex (`WatchedAfterSettle`, #9724). Imports of partial 64 KiB blocks get no deferred read; only that path reads them, returning partial blocks whole as CPU stores unless a live, trimmed or parked compared import or storage image shares them; one a recorder holds is unseen. The latency cost of placing batches behind main-queue work is unmeasured. The first-use test now checks that earlier main-queue reads finish before the import returns to write watching; the original two-queue canary measured driver marking while those reads were still in flight. The earlier-import canary still checks later use of another import of the same pages. - [Settled dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a dma-buf import of 1 MiB or more is read by a 4-byte copy on the settle queue right after it is made (`settleImport`), so the kernel maps its pages for the GPU there and not in the first submission that uses it (about 2 s for a 3 GiB import on RADV). The import's buffer is shared with the settle queue's family. An import made at map time settles on the guest thread that maps the range, inside its map call, so that thread waits while the kernel maps the pages: 0.47 to 2.2 s for the 3 GiB range of Hades (PPSA03355) and 0.49 to 4.3 s for the 4.5 GiB range of ASTRO BOT (PPSA21567) on RADV. Both titles map these ranges at start-up; a title that maps large ranges during gameplay stalls its mapping thread for as long. An import made from a submission settles under the GPU mutex, so its frame still pays for it; a smaller import, and every import on a device with no settle queue (one without a copy-capable queue besides the main one), pays in its first submission. A range imported through a host pointer, as lavapipe imports shared memory, is not settled - [Sparse dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a shared-backed range larger than one 64 MiB udmabuf buffer is imported as one sparse buffer bound to the 64 MiB buffers, on a queue of its own when the device has a second family that binds sparse memory. The path is decided before anything is imported, and a failure after that throws. A range whose offset in the backing file is not a multiple of 64 KiB starts its sparse buffer at the 64 KiB block below it (`HostImport::lead`). A range takes the single udmabuf import instead, which udmabuf's `size_limit_mb` refuses above 64 MiB by default so the range is copied, when the buffer's sparse block size is larger than 64 KiB (it is 64 KiB on RADV), when the sparse buffer does not accept every memory type a chunk import can have, when the backing file ends inside its last chunk, or when it is imported at map time on a device with no second family (the bind then needs the GPU mutex); the NVIDIA proprietary driver imports no dma-buf memory - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. From bef24410afb833ef5d1dfa40f7738263c15e13db Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 22 Oct 2026 22:09:38 +0000 Subject: [PATCH 158/428] fix(agc): settle a pending import before the FMASK checks of an initial multisampled DCC import #7948 requires the FMASK of an eight-fragment DCC target to be write-watched at the initial import and at each reuse. On Windows an FMASK in a host import whose deferred settle has not run yet is still compared, so the target is refused with "native FMASK contents require watched guest memory" after the previous commit has settled its color and metadata ranges. Call WatchedAfterSettle for the FMASK range at both checks, as the initial import already does for the color, DCC and CMASK ranges. A range that is unwatched without an import awaiting its read is still refused, and nothing changes off Windows. Four-sample targets take no FMASK range, so this changes nothing for them. Refs https://github.com/boykopovar/AnyPS5/issues/9724 --- .../Graphics/src/MultisampleTarget.cpp | 4 +- .../prx/libSceAgcDriver/tests/Recorder.cpp | 53 ++++++++++++++++--- docs/dev/debt/libSceAgcDriver.md | 2 +- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 90f04b9c1f..6f5a7b0339 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -592,7 +592,7 @@ private: fmaskRange = GuestMemory::UnchangedQuery{use.fmaskAddress, static_cast(DescribeSurface(resource).guestBytes), 0}; auto& range = *fmaskRange; GuestMemory::CheckRange(reinterpret_cast(range.address), range.bytes, 65536u); - Require(GuestMemory::Watched(range.address, range.bytes), "native FMASK contents require watched guest memory"); + Require(WatchedAfterSettle(context, range.address, range.bytes), "native FMASK contents require watched guest memory"); range.generation = GuestMemory::CollectWritesUncached(range.address, range.bytes); Require(range.generation != 0, "native FMASK contents have no write generation"); for (const auto& other : dccRanges) Require(range.address >= other.address + other.bytes || other.address >= range.address + range.bytes, "native FMASK overlaps its color or metadata planes"); @@ -632,7 +632,7 @@ private: if (fmaskRange) { Require(use.fmaskAddress == target.fmaskAddress && use.fmaskPipeBankXor == target.fmaskPipeBankXor, "native FMASK target layout changed"); const auto& range = *fmaskRange; - requireDccRange(GuestMemory::Watched(range.address, range.bytes), range, "native FMASK sampling requires watched guest memory"); + requireDccRange(WatchedAfterSettle(context, range.address, range.bytes), range, "native FMASK sampling requires watched guest memory"); GuestMemory::CollectWritesUncached(range.address, range.bytes); requireDccRange(GuestMemory::UnchangedSince(range.address, range.bytes, range.generation) || !GuestMemory::StoredOver(range.address, range.bytes, range.generation), range, "writes to a resident native FMASK are not implemented"); if (auto* recorder = Recorder::Active(); recorder != nullptr) requireDccRange(!recorder->PendingWriteOverlaps(range.address, range.bytes), range, "pending writes to a resident native FMASK are not implemented"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index a18bfd9ee2..de892f210f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2542,28 +2542,35 @@ bool settleInitialDccTests(const Device& device) { context.detiler = &detiler; Recorder recorder(context); recorder.Activate(); - enum class Case { Pending, PartialBlock, ComparedNeighbour, DeferredOff, UnwatchedKeys }; + enum class Case { Pending, PartialBlock, ComparedNeighbour, DeferredOff, UnwatchedKeys, PendingFmask }; const auto run = [&](Case kind, std::uint32_t height) { + if (kind == Case::PendingFmask && (!base.storageImageMultisample || (base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_8_BIT) == 0)) { + std::cout << "(m) 8x color or multisampled storage images unavailable: an initial FMASK import over a pending settle not tested\n"; + return; + } const bool partial = kind == Case::PartialBlock || kind == Case::ComparedNeighbour; ColorTarget target{}; target.extent = {128, height}; target.surfaceExtent = target.extent; target.format = VK_FORMAT_R8G8B8A8_UNORM; target.elementBytes = 4; - target.samples = 4; + target.samples = kind == Case::PendingFmask ? 8 : 4; target.tileMode = ColorTileMode::RenderTarget; target.dccPipeAligned = true; const auto colorBytes = static_cast(MultisampledColorBytes(target)); const auto dccBytes = MultisampledDccBytes(target); const auto importBytes = colorBytes + (partial ? tail : unit); const auto reserved = (importBytes + unit - 1) & ~(unit - 1); - const auto regionBytes = unit + reserved + unit + (kind == Case::UnwatchedKeys ? unit : 0); + const auto fmaskBytes = static_cast(target.extent.width) * target.extent.height * 4u; + const auto fmaskReserved = (fmaskBytes + unit - 1) & ~(unit - 1); + const auto regionBytes = unit + reserved + unit + (kind == Case::UnwatchedKeys ? unit : kind == Case::PendingFmask ? fmaskReserved + unit : 0); auto* region = static_cast(AllocateWatched(regionBytes, unit)); void* block = region != nullptr ? region + unit : nullptr; void* keys = region != nullptr && kind == Case::UnwatchedKeys ? region + unit + reserved + unit : nullptr; + void* fmask = region != nullptr && kind == Case::PendingFmask ? region + unit + reserved + unit : nullptr; const auto destBytes = static_cast(target.extent.width) * target.extent.height * 4u; void* dest = VirtualAlloc(nullptr, destBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); - Require(block != nullptr && dest != nullptr && (kind != Case::UnwatchedKeys || keys != nullptr), "(m) cannot allocate the target"); + Require(block != nullptr && dest != nullptr && (kind != Case::UnwatchedKeys || keys != nullptr) && (kind != Case::PendingFmask || fmask != nullptr), "(m) cannot allocate the target"); const auto address = reinterpret_cast(block); const auto neighbour = address + importBytes + tail; const auto describe = [](const HostImport* found, std::uint64_t at, std::size_t bytes) { @@ -2579,6 +2586,7 @@ bool settleInitialDccTests(const Device& device) { std::size_t regionBytes; void* block; void* keys; + void* fmask; void* dest; std::size_t reserved; std::uint64_t neighbour; @@ -2595,6 +2603,7 @@ bool settleInitialDccTests(const Device& device) { mutation.Remove(block); mutation.Remove(dest); if (keys != nullptr) mutation.Remove(keys); + if (fmask != nullptr) mutation.Remove(fmask); if (neighbour < reinterpret_cast(block) + reserved) mutation.Remove(reinterpret_cast(neighbour)); } HostImportFor(context, reinterpret_cast(block), 16); @@ -2602,7 +2611,7 @@ bool settleInitialDccTests(const Device& device) { ReleaseWatched(region, regionBytes); VirtualFree(dest, 0, MEM_RELEASE); } - } release{context, recorder, region, regionBytes, block, keys, dest, reserved, kind == Case::ComparedNeighbour ? neighbour : std::numeric_limits::max()}; + } release{context, recorder, region, regionBytes, block, keys, fmask, dest, reserved, kind == Case::ComparedNeighbour ? neighbour : std::numeric_limits::max()}; target.address = address; target.surfaceAddress = address; target.bytes = colorBytes; @@ -2610,11 +2619,21 @@ bool settleInitialDccTests(const Device& device) { std::memset(block, 0, importBytes); std::memset(reinterpret_cast(target.dccAddress), 0xc0, dccBytes); std::memset(dest, 0x5a, destBytes); + if (fmask != nullptr) { + target.cmaskAddress = target.dccAddress + ((dccBytes + 4095) & ~std::size_t{4095}); + target.cmaskBytes = CmaskBytes(target.extent.width, target.extent.height); + target.cmaskFastClear = true; + target.fmaskAddress = reinterpret_cast(fmask); + Require(target.cmaskAddress + target.cmaskBytes <= address + importBytes, "(m) the CMASK does not fit the target's import"); + std::memset(reinterpret_cast(target.cmaskAddress), 0xcc, static_cast(target.cmaskBytes)); + std::memset(fmask, 0x11, fmaskBytes); + } { GuestAllocations::Mutation mutation; mutation.Add(block, importBytes, true, true, true); mutation.Add(dest, destBytes, true, true, true); if (keys != nullptr) mutation.Add(keys, unit, true, true, true); + if (fmask != nullptr) mutation.Add(fmask, fmaskBytes, true, true, true); if (kind == Case::ComparedNeighbour) mutation.Add(reinterpret_cast(neighbour), reserved - (importBytes + tail), true, true, true); } if (kind == Case::UnwatchedKeys) Unwatch(target.dccAddress, dccBytes); @@ -2623,6 +2642,10 @@ bool settleInitialDccTests(const Device& device) { const auto* other = HostImportFor(context, neighbour, reserved - (importBytes + tail)); Require(other != nullptr && other->unwatched && !Watched(neighbour, reserved - (importBytes + tail)), "(m) the import sharing the target's last 64 KiB block was not compared" + describe(other, neighbour, reserved - (importBytes + tail))); } + if (fmask != nullptr) { + const auto* fmaskImport = HostImportFor(context, target.fmaskAddress, fmaskBytes); + Require(fmaskImport != nullptr && fmaskImport->base == target.fmaskAddress && fmaskImport->bytes == fmaskBytes && fmaskImport->unwatched && !Watched(target.fmaskAddress, fmaskBytes), "(m) the FMASK's import was not compared at first" + describe(fmaskImport, target.fmaskAddress, fmaskBytes)); + } const auto* import = HostImportFor(context, address, importBytes); Require(import != nullptr && import->base == address && import->bytes == importBytes && import->unwatched && !Watched(address, importBytes), "(m) the target's import of " + std::to_string(importBytes) + " bytes was not compared at first" + describe(import, address, importBytes)); std::unique_lock gpu(GpuMutex()); @@ -2644,7 +2667,7 @@ bool settleInitialDccTests(const Device& device) { } const auto after = HostImportSettles(); const auto* settled = ExistingHostImportFor(context, address, importBytes); - const char* name = kind == Case::Pending ? "a pending settle" : kind == Case::PartialBlock ? "an import of whole and partial 64 KiB blocks" : kind == Case::ComparedNeighbour ? "a compared import in its last 64 KiB block" : kind == Case::DeferredOff ? "deferred settles off" : "explicitly unwatched DCC keys"; + const char* name = kind == Case::Pending ? "a pending settle" : kind == Case::PartialBlock ? "an import of whole and partial 64 KiB blocks" : kind == Case::ComparedNeighbour ? "a compared import in its last 64 KiB block" : kind == Case::DeferredOff ? "deferred settles off" : kind == Case::UnwatchedKeys ? "explicitly unwatched DCC keys" : "an eight-sample target whose FMASK has its own pending settle"; std::cout << "(m) initial DCC over " << name << " (import " << importBytes << " bytes): " << (refusal.empty() ? "accepted" : refusal) << "; " << after.submitted - before.submitted << " settle reads submitted, " << after.rewatched - before.rewatched << " back in the write watch\n"; if (kind == Case::ComparedNeighbour || kind == Case::DeferredOff || kind == Case::UnwatchedKeys) { Require(refusal.find("initial multisampled DCC contents require watched guest memory") != std::string::npos, std::string("(m) an initial multisampled DCC import over ") + name + " was not refused: " + refusal); @@ -2654,6 +2677,10 @@ bool settleInitialDccTests(const Device& device) { } Require(refusal.empty(), std::string("(m) an initial multisampled DCC import over ") + name + " was refused: " + refusal); Require(after.submitted > before.submitted && after.rewatched > before.rewatched && Watched(address, importBytes) && settled != nullptr && !settled->unwatched, std::string("(m) the initial DCC import over ") + name + " did not settle the import and return it to the write watch"); + if (fmask != nullptr) { + const auto* settledFmask = ExistingHostImportFor(context, target.fmaskAddress, fmaskBytes); + Require(Watched(target.fmaskAddress, fmaskBytes) && settledFmask != nullptr && !settledFmask->unwatched, "(m) the FMASK's import did not settle back into the write watch" + describe(settledFmask, target.fmaskAddress, fmaskBytes)); + } ColorTarget destination{}; destination.address = reinterpret_cast(dest); destination.surfaceAddress = destination.address; @@ -2672,6 +2699,19 @@ bool settleInitialDccTests(const Device& device) { AgcDriver::GuestMemory::Read(destination.address, std::as_writable_bytes(std::span(resolved))); Require(std::ranges::all_of(resolved, [](std::uint32_t word) { return word == 0xffffffffu; }), std::string("(m) the target made over ") + name + " did not start from its DCC fast clear"); BumpCollectEpoch(); + if (fmask != nullptr) { + const auto collectedFmask = CollectWrites(target.fmaskAddress, fmaskBytes); + Require(collectedFmask != 0, "(m) the settled FMASK import is not collected"); + static_cast(fmask)[0] = 0x22; + std::string fmaskReuse; + try { + MultisampleTargetView(context, target); + } catch (const std::exception& error) { + fmaskReuse = error.what(); + } + Require(fmaskReuse.find("writes to a resident native FMASK are not implemented") != std::string::npos, "(m) the resident target missed the CPU store to its FMASK: " + fmaskReuse); + return; + } const auto collected = CollectWrites(address, importBytes); Require(collected != 0, "(m) the settled import is not collected"); static_cast(block)[importBytes - 1] = 0x77; @@ -2691,6 +2731,7 @@ bool settleInitialDccTests(const Device& device) { run(Case::ComparedNeighbour, 384); run(Case::DeferredOff, 128); run(Case::UnwatchedKeys, 128); + run(Case::PendingFmask, 128); std::cout << "initial multisampled DCC over a pending settle tests passed\n"; return true; } diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 2aacf360dc..42c2528c6a 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -165,7 +165,7 @@ - [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces still throw: hardware load bounds and filtered-sample addressing differ. A range of six faces or more that does not end on a whole cube keeps every slice up to `DEPTH`, the bound loads use on hardware; sampling the cube it cuts short is out-of-range cube-array sampling. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. - [Array layer views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (libSceAgcDriver) - `ArrayLayersServeView` lets a 2D texture at a 2D array image's base address, or a 2D array texture over its first layers, view that image (`Texture::CanCopyFrom`). Mesa-built homebrew (ps5-opengl) writes the view's last slice plus one to `CB_COLOR_ATTRIB3.MIP0_DEPTH`, so a slice-0 render target is a two-layer array image, and the title samples it as a plain 2D texture (SuperTuxKart PPSA69421 drew a black 3D scene and minimap without this). The per-layer layout of a mipmapped array is assumed equal to a standalone surface's; it is not measured on the console. A 2D texture at a later slice's address still gets an image of its own. - [Multisampled CMASK](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size. -- [Compressed FMASK reads](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) without an exact resident native owner throw. Native reads require eight fragments, one slice and mip, pipe-aligned DCC/CMASK, Z_64KB_X FMASK and unchanged, disjoint watched planes. The host stores independent samples and exposes their expanded indices (`0x76543210`, AMD PAL `c5e80007`, `Gfx9Fmask::GetPackedExpandedValue`); console equivalence is unmeasured. Other layouts, standalone compressed decoding, guest publication and recipe reuse remain unsupported. Metadata writes invalidate the native view, including same-value stores. +- [Compressed FMASK reads](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) without an exact resident native owner throw. Native reads require eight fragments, one slice and mip, pipe-aligned DCC/CMASK, Z_64KB_X FMASK and unchanged, disjoint watched planes; an FMASK import awaiting its Windows deferred read is read first (`WatchedAfterSettle`). The host stores independent samples and exposes their expanded indices (`0x76543210`, AMD PAL `c5e80007`, `Gfx9Fmask::GetPackedExpandedValue`); console equivalence is unmeasured. Other layouts, standalone compressed decoding, guest publication and recipe reuse remain unsupported. Metadata writes invalidate the native view, including same-value stores. - [Multisampled CMASK state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target has no guest texels, so its CMASK state comes from fills rather than from a read at each draw as a single-sample target's does. A fill of the whole CMASK with 0 clears the host image at the target's next use, one with 0xffffffff marks it expanded, and any other pattern, or a fill that covers only part of the CMASK, throws at the next use (inferred: main's rule that a mixed CMASK is unmodeled, applied to the one host image). A FAST_CLEAR target starts from its CMASK keys at its first use, and a COMPRESSION-only target starts expanded. Copies by DMA into the CMASK are not seen, and a later use with another CMASK address throws. The clear does not rewrite the CMASK bytes as expanded, so a new target over the same CMASK (a different extent, so a different image) seeds from them again and clears once more. - [Multisampled DCC CMASK without fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the initial CMASK of a multisampled DCC target is not checked when CB_COLOR_INFO enables COMPRESSION but not FAST_CLEAR: it then holds only FMASK compression, and a uniform DCC clear sets every fragment, so the target imports as that clear. What the console reads where such an FMASK names no fragment, as an uninitialized CMASK can, is unverified. - [Multisampled color fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target's samples are on the host, so a fill of its color memory is applied at its next use: a uniform fill over the whole of it clears the host image to that value, and a pending CMASK fast clear takes precedence over the fill and over a fill that would throw. Its color memory is a single mip and slice of addrlib's block dimensions for its sample count (ComputeThinBlockDimension). A fill over part of it, a fill reaching the start of a target whose footprint is not computed (linear), and a fill value with no exact clear value in the format (sRGB channels other than 0 and 1) throw at the next use. A fill before the target's first use is not seen (the host image starts at 0), nor are copies or shader writes into its color memory. A uniform compute fill over exactly the whole color memory with an exact clear value is not stored into guest memory, since only the host image holds the samples. A target is retired only when its whole guarded range is unmapped or its allocation removed, so if a title reuses that memory for other data while it stays mapped, a read the guard does not check (a buffer read through a V# or a pointer) sees the bytes from before the fill. From e551d072749d42a6deef31aea552f6192058ccc1 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 23 Oct 2026 02:05:42 +0000 Subject: [PATCH 159/428] test(agc): keep only the FMASK's settle pending in an eight-sample initial DCC case Case 6 of settleInitialDccTests imports the target's color, DCC and CMASK region and its FMASK region off the GPU lock, so both settles are pending at the first use. The color loop's WatchedAfterSettle submits a settle batch, and submitSettleBatch puts every wanted settle in it, the FMASK's included. By the FMASK check the FMASK is back in the write watch, so the case passes with and without the previous commit: 2 settle reads submitted either way on an RTX 3070. Keep case 6 as the case where both settles are pending, and add a case where only the FMASK's is. The target's import is made and settled under the GPU lock with WatchedAfterSettle before the FMASK's import exists, so its color, DCC and CMASK planes are watched with no compared import over them. The FMASK then gets its own 64 KiB import off the GPU lock, between unregistered 64 KiB gaps, compared with its deferred settle wanted. The case checks those preconditions and that no settle read is submitted before the first use. The settle counters do not tell the color loop from the FMASK check, so the case requires exactly one settle read for the whole use: the FMASK's. With the previous commit the target is accepted and its FMASK returns to the write watch. Without it the FMASK check refuses the target with "native FMASK contents require watched guest memory" and no settle read is submitted. Refs https://github.com/boykopovar/AnyPS5/issues/9724 --- .../prx/libSceAgcDriver/tests/Recorder.cpp | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index de892f210f..b0ab851dce 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2542,9 +2542,11 @@ bool settleInitialDccTests(const Device& device) { context.detiler = &detiler; Recorder recorder(context); recorder.Activate(); - enum class Case { Pending, PartialBlock, ComparedNeighbour, DeferredOff, UnwatchedKeys, PendingFmask }; + enum class Case { Pending, PartialBlock, ComparedNeighbour, DeferredOff, UnwatchedKeys, PendingFmask, PendingFmaskAlone }; const auto run = [&](Case kind, std::uint32_t height) { - if (kind == Case::PendingFmask && (!base.storageImageMultisample || (base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_8_BIT) == 0)) { + const bool fmaskAlone = kind == Case::PendingFmaskAlone; + const bool eightSamples = kind == Case::PendingFmask || fmaskAlone; + if (eightSamples && (!base.storageImageMultisample || (base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_8_BIT) == 0)) { std::cout << "(m) 8x color or multisampled storage images unavailable: an initial FMASK import over a pending settle not tested\n"; return; } @@ -2554,7 +2556,7 @@ bool settleInitialDccTests(const Device& device) { target.surfaceExtent = target.extent; target.format = VK_FORMAT_R8G8B8A8_UNORM; target.elementBytes = 4; - target.samples = kind == Case::PendingFmask ? 8 : 4; + target.samples = eightSamples ? 8 : 4; target.tileMode = ColorTileMode::RenderTarget; target.dccPipeAligned = true; const auto colorBytes = static_cast(MultisampledColorBytes(target)); @@ -2563,20 +2565,20 @@ bool settleInitialDccTests(const Device& device) { const auto reserved = (importBytes + unit - 1) & ~(unit - 1); const auto fmaskBytes = static_cast(target.extent.width) * target.extent.height * 4u; const auto fmaskReserved = (fmaskBytes + unit - 1) & ~(unit - 1); - const auto regionBytes = unit + reserved + unit + (kind == Case::UnwatchedKeys ? unit : kind == Case::PendingFmask ? fmaskReserved + unit : 0); + const auto regionBytes = unit + reserved + unit + (kind == Case::UnwatchedKeys ? unit : eightSamples ? fmaskReserved + unit : 0); auto* region = static_cast(AllocateWatched(regionBytes, unit)); void* block = region != nullptr ? region + unit : nullptr; void* keys = region != nullptr && kind == Case::UnwatchedKeys ? region + unit + reserved + unit : nullptr; - void* fmask = region != nullptr && kind == Case::PendingFmask ? region + unit + reserved + unit : nullptr; + void* fmask = region != nullptr && eightSamples ? region + unit + reserved + unit : nullptr; const auto destBytes = static_cast(target.extent.width) * target.extent.height * 4u; void* dest = VirtualAlloc(nullptr, destBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); - Require(block != nullptr && dest != nullptr && (kind != Case::UnwatchedKeys || keys != nullptr) && (kind != Case::PendingFmask || fmask != nullptr), "(m) cannot allocate the target"); + Require(block != nullptr && dest != nullptr && (kind != Case::UnwatchedKeys || keys != nullptr) && (!eightSamples || fmask != nullptr), "(m) cannot allocate the target"); const auto address = reinterpret_cast(block); const auto neighbour = address + importBytes + tail; const auto describe = [](const HostImport* found, std::uint64_t at, std::size_t bytes) { - char text[224]; + char text[256]; if (found == nullptr) std::snprintf(text, sizeof(text), ": no import of 0x%llx+0x%zx, watched %d", static_cast(at), bytes, Watched(at, bytes) ? 1 : 0); - else std::snprintf(text, sizeof(text), ": import 0x%llx+0x%llx, unwatched %d, for 0x%llx+0x%zx, watched %d", static_cast(found->base), static_cast(found->bytes), found->unwatched ? 1 : 0, static_cast(at), bytes, Watched(at, bytes) ? 1 : 0); + else std::snprintf(text, sizeof(text), ": import 0x%llx+0x%llx, unwatched %d, settled on demand %d, for 0x%llx+0x%zx, watched %d", static_cast(found->base), static_cast(found->bytes), found->unwatched ? 1 : 0, found->settleOnDemand ? 1 : 0, static_cast(at), bytes, Watched(at, bytes) ? 1 : 0); return std::string(text); }; struct Release { @@ -2642,12 +2644,30 @@ bool settleInitialDccTests(const Device& device) { const auto* other = HostImportFor(context, neighbour, reserved - (importBytes + tail)); Require(other != nullptr && other->unwatched && !Watched(neighbour, reserved - (importBytes + tail)), "(m) the import sharing the target's last 64 KiB block was not compared" + describe(other, neighbour, reserved - (importBytes + tail))); } - if (fmask != nullptr) { + if (fmask != nullptr && !fmaskAlone) { const auto* fmaskImport = HostImportFor(context, target.fmaskAddress, fmaskBytes); Require(fmaskImport != nullptr && fmaskImport->base == target.fmaskAddress && fmaskImport->bytes == fmaskBytes && fmaskImport->unwatched && !Watched(target.fmaskAddress, fmaskBytes), "(m) the FMASK's import was not compared at first" + describe(fmaskImport, target.fmaskAddress, fmaskBytes)); } const auto* import = HostImportFor(context, address, importBytes); Require(import != nullptr && import->base == address && import->bytes == importBytes && import->unwatched && !Watched(address, importBytes), "(m) the target's import of " + std::to_string(importBytes) + " bytes was not compared at first" + describe(import, address, importBytes)); + HostImportSettleCounts quiet{}; + if (fmaskAlone) { + { + std::lock_guard gpu(GpuMutex()); + const auto start = HostImportSettles(); + Require(WatchedAfterSettle(context, address, importBytes), "(m) the target's import did not settle before the FMASK's import" + describe(ExistingHostImportFor(context, address, importBytes), address, importBytes)); + const auto settledTarget = HostImportSettles(); + Require(settledTarget.submitted > start.submitted && settledTarget.rewatched > start.rewatched, "(m) the target's import returned to the write watch without a settle read"); + } + const std::array, 3> planes{{{address, colorBytes}, {target.dccAddress, dccBytes}, {target.cmaskAddress, static_cast(target.cmaskBytes)}}}; + for (const auto& [at, bytes] : planes) { + const auto* covering = ExistingHostImportFor(context, at, bytes); + Require(Watched(at, bytes) && (covering == nullptr || !covering->unwatched), "(m) a color, DCC or CMASK plane is not watched before the FMASK's import" + describe(covering, at, bytes)); + } + quiet = HostImportSettles(); + const auto* fmaskImport = HostImportFor(context, target.fmaskAddress, fmaskBytes); + Require(fmaskImport != nullptr && fmaskImport->base == target.fmaskAddress && fmaskImport->bytes == fmaskBytes && fmaskImport->unwatched && !fmaskImport->settleOnDemand && !Watched(target.fmaskAddress, fmaskBytes), "(m) the FMASK's import is not compared with its deferred settle wanted" + describe(fmaskImport, target.fmaskAddress, fmaskBytes)); + } std::unique_lock gpu(GpuMutex()); if (partial) { for (int round = 0; round < 20; ++round) { @@ -2659,6 +2679,7 @@ bool settleInitialDccTests(const Device& device) { Require(!Watched(address, importBytes), "(m) a deferred settle returned an import that is not a whole number of 64 KiB blocks to the write watch"); } const auto before = HostImportSettles(); + Require(!fmaskAlone || before.submitted == quiet.submitted, "(m) a settle read was submitted between the FMASK's import and the target's first use"); std::string refusal; try { Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "(m) the multisampled DCC target has no view"); @@ -2667,7 +2688,7 @@ bool settleInitialDccTests(const Device& device) { } const auto after = HostImportSettles(); const auto* settled = ExistingHostImportFor(context, address, importBytes); - const char* name = kind == Case::Pending ? "a pending settle" : kind == Case::PartialBlock ? "an import of whole and partial 64 KiB blocks" : kind == Case::ComparedNeighbour ? "a compared import in its last 64 KiB block" : kind == Case::DeferredOff ? "deferred settles off" : kind == Case::UnwatchedKeys ? "explicitly unwatched DCC keys" : "an eight-sample target whose FMASK has its own pending settle"; + const char* name = kind == Case::Pending ? "a pending settle" : kind == Case::PartialBlock ? "an import of whole and partial 64 KiB blocks" : kind == Case::ComparedNeighbour ? "a compared import in its last 64 KiB block" : kind == Case::DeferredOff ? "deferred settles off" : kind == Case::UnwatchedKeys ? "explicitly unwatched DCC keys" : kind == Case::PendingFmask ? "an eight-sample target whose color and FMASK imports both have pending settles" : "an eight-sample target whose FMASK alone has a pending settle"; std::cout << "(m) initial DCC over " << name << " (import " << importBytes << " bytes): " << (refusal.empty() ? "accepted" : refusal) << "; " << after.submitted - before.submitted << " settle reads submitted, " << after.rewatched - before.rewatched << " back in the write watch\n"; if (kind == Case::ComparedNeighbour || kind == Case::DeferredOff || kind == Case::UnwatchedKeys) { Require(refusal.find("initial multisampled DCC contents require watched guest memory") != std::string::npos, std::string("(m) an initial multisampled DCC import over ") + name + " was not refused: " + refusal); @@ -2676,6 +2697,7 @@ bool settleInitialDccTests(const Device& device) { return; } Require(refusal.empty(), std::string("(m) an initial multisampled DCC import over ") + name + " was refused: " + refusal); + Require(!fmaskAlone || (after.submitted - before.submitted == 1 && after.rewatched - before.rewatched == 1), "(m) the first use over a pending FMASK settle alone submitted " + std::to_string(after.submitted - before.submitted) + " settle reads, not the FMASK's alone"); Require(after.submitted > before.submitted && after.rewatched > before.rewatched && Watched(address, importBytes) && settled != nullptr && !settled->unwatched, std::string("(m) the initial DCC import over ") + name + " did not settle the import and return it to the write watch"); if (fmask != nullptr) { const auto* settledFmask = ExistingHostImportFor(context, target.fmaskAddress, fmaskBytes); @@ -2732,6 +2754,7 @@ bool settleInitialDccTests(const Device& device) { run(Case::DeferredOff, 128); run(Case::UnwatchedKeys, 128); run(Case::PendingFmask, 128); + run(Case::PendingFmaskAlone, 128); std::cout << "initial multisampled DCC over a pending settle tests passed\n"; return true; } From 5537cb82c08485390b98727a845027a576a169c7 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Fri, 23 Oct 2026 00:45:52 -0300 Subject: [PATCH 160/428] test(agc): cover multi-batch import settling --- .../prx/libSceAgcDriver/tests/Recorder.cpp | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index b0ab851dce..1395c85420 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2537,6 +2537,57 @@ bool settleInitialDccTests(const Device& device) { } } std::cout << "write watch: Windows private imports stay watched after a settle read; imports under 1 MiB wait for a deferred settle\n"; + for (const std::size_t count : {256u, 257u, 1025u}) { + const auto bytes = count * unit; + auto* block = static_cast(AllocateWatched(bytes, unit)); + Require(block != nullptr, "(m) cannot allocate adjacent pending imports"); + const auto address = reinterpret_cast(block); + struct Release { + const Context& context; + std::uint8_t* block; + std::size_t bytes; + std::size_t registered = 0; + ~Release() { + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < registered; ++index) mutation.Remove(block + index * unit); + } + HostImportFor(context, reinterpret_cast(block), unit); + SetHostImportWindow(0); + ReleaseWatched(block, bytes); + } + } release{base, block, bytes}; + std::memset(block, 0x11, bytes); + SetHostImportWindow(unit); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < count; ++index) { + mutation.Add(block + index * unit, unit, true, true, true); + ++release.registered; + } + } + for (std::size_t index = 0; index < count; ++index) { + const auto at = address + index * unit; + const auto* import = HostImportFor(base, at, unit); + Require(import != nullptr && import->base == at && import->bytes == unit && import->unwatched, + "(m) an adjacent 64 KiB import did not start with its settle pending"); + } + { + std::lock_guard gpu(GpuMutex()); + Require(!Watched(address, bytes), "(m) adjacent pending imports were already watched"); + const auto before = HostImportSettles(); + Require(WatchedAfterSettle(base, address, bytes), + "(m) one synchronous settle left part of " + std::to_string(count) + " adjacent imports unwatched"); + const auto after = HostImportSettles(); + BumpCollectEpoch(); + const auto collected = CollectWrites(address, bytes); + Require(collected != 0, "(m) adjacent settled imports were not collected"); + static_cast(block)[bytes - 1] = 0x22; + CollectWritesUncached(address, bytes); + Require(!UnchangedSince(address, bytes, collected), "(m) a CPU write after the final import settled was missed"); + std::cout << "(m) " << count << " adjacent pending imports settled: " << after.submitted - before.submitted << " reads submitted\n"; + } + } TextureDetiler detiler(base); auto context = base; context.detiler = &detiler; From f3964a7d2d33c33fa9bd6766642b22263ea4b1c3 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Fri, 23 Oct 2026 00:49:52 -0300 Subject: [PATCH 161/428] fix(agc): settle every pending import batch --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 +- .../libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 3506da9069..466d149d3c 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1539,7 +1539,7 @@ if(BUILD_TESTING) add_test(NAME agc_import_watch COMMAND agc_driver_recorder_tests --import-watch-only) set_tests_properties(agc_import_watch PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_import_settle_initial_dcc COMMAND agc_driver_recorder_tests --settle-initial-dcc-only) - set_tests_properties(agc_import_settle_initial_dcc PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) + set_tests_properties(agc_import_settle_initial_dcc PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) add_test(NAME agc_import_watch_other_family COMMAND agc_driver_recorder_tests --import-watch-other-family-only) set_tests_properties(agc_import_watch_other_family PROPERTIES SKIP_RETURN_CODE 77) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index e8974f1e34..95369e66b0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1177,7 +1177,7 @@ void settleImportsNow(const Context& context, HostImports& state, std::uint64_t return settle != state.pendingSettles.end() && settle->second.phase == SettlePhase::Wanted; }); }; - if (wanted()) { + while (wanted()) { if (std::all_of(deviceSettle.batches.begin(), deviceSettle.batches.end(), [](const SettleBatch& batch) { return batch.busy; })) { for (const auto& batch : deviceSettle.batches) waitSettleBatch(deviceSettle, context.device, batch); reapSettleBatches(context, state); From e172ee839448a041ee5cb1f64af17959125f2b96 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 23 Oct 2026 08:57:52 +0000 Subject: [PATCH 162/428] fix(agc): let Rewatch return page-aligned ranges Rewatch took only whole 64 KiB blocks, so an unwatched import whose size or base is not a block multiple could only be offered its rounded span, and that span reaches pages the import does not own: uncommitted ones fail the write-watch walk and nothing returns, committed ones of a neighbour return with it. Rewatch now takes ranges in 4 KiB pages, as WriteWatchCoverage::Restore already does, and still stamps every block a range touches as a CPU store. agc_write_tracking_rewatch_import_pages (Windows) covers a 0xc4000-byte import, one starting 0x1000 into a block, one sharing its last block with another unwatched import, and a whole-block import. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +- .../Execution/src/GuestMemory.cpp | 2 +- .../libSceAgcDriver/tests/WriteTracking.cpp | 133 +++++++++++++++++- .../tests/WriteWatchCoverage.cpp | 11 ++ 4 files changed, 146 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 466d149d3c..7e6d6174e7 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -771,7 +771,9 @@ if(BUILD_TESTING) set_tests_properties(agc_write_tracking_metadata_commit PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) add_test(NAME agc_resident_direct_remap COMMAND agc_write_tracking_tests --resident-direct-remap) set_tests_properties(agc_resident_direct_remap PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 20) - set_tests_properties(agc_write_tracking agc_write_tracking_metadata_commit agc_resident_direct_remap PROPERTIES ENVIRONMENT_MODIFICATION + add_test(NAME agc_write_tracking_rewatch_import_pages COMMAND agc_write_tracking_tests --rewatch-import-pages) + set_tests_properties(agc_write_tracking_rewatch_import_pages PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 20) + set_tests_properties(agc_write_tracking agc_write_tracking_metadata_commit agc_resident_direct_remap agc_write_tracking_rewatch_import_pages PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 960cf4a11c..b4f6624047 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1656,7 +1656,7 @@ void Rewatch(std::span> ranges) { if (!tracker.watched) return; bool restored = false; for (const auto& [begin, end] : ranges) { - if (end <= begin || begin < tracker.base || end - tracker.base > tracker.size || (begin - tracker.base) % WriteBlockBytes != 0 || (end - tracker.base) % WriteBlockBytes != 0) continue; + if (end <= begin || begin < tracker.base || end - tracker.base > tracker.size || (begin - tracker.base) % WritePageBytes != 0 || (end - tracker.base) % WritePageBytes != 0) continue; const auto bytes = static_cast(end - begin); if (tracker.coverage.Covers(begin, bytes) || !walkWrites(tracker, begin, end, StampKind::Cpu) || !tracker.coverage.Restore(begin, bytes)) continue; ++tracker.generation; diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index 267c945d64..692200007e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -35,6 +35,7 @@ constexpr std::size_t Block = 65536; #ifdef _WIN32 constexpr wchar_t HostHoleMode[] = L"--host-hole"; constexpr wchar_t ResidentRemapMode[] = L"--resident-direct-remap"; +constexpr wchar_t RewatchImportPagesMode[] = L"--rewatch-import-pages"; #endif void Require(bool condition, const std::string& message) { @@ -708,13 +709,18 @@ void CheckRewatch() { const auto unwatched = UnwatchSerial(); Require(!Watched(imported, Block) && UntrackedHint(imported, Block) != 0, "the unwatched block does not read as untracked"); bytes[Block + 8] = 0x22; + const std::array, 1> unaligned{{{imported + 1, imported + Block}}}; + Rewatch(unaligned); + Require(!Watched(imported + 4096, Block - 4096) && UnwatchSerial() == unwatched, "a range that is not whole pages returned to the write watch"); const std::array, 1> partial{{{imported + 4096, imported + Block}}}; Rewatch(partial); - Require(!Watched(imported, Block) && UnwatchSerial() == unwatched, "part of a block returned to the write watch"); + Require(Watched(imported + 4096, Block - 4096) && !Watched(imported, 4096) && !Watched(imported, Block), "rewatching part of a block did not return exactly its pages to the write watch"); + const auto partly = UnwatchSerial(); + Require(partly != unwatched, "rewatching part of a block did not invalidate what was cached while it was unwatched"); const std::array, 1> first{{{imported, imported + Block}}}; Rewatch(first); Require(Watched(imported, Block) && !Watched(imported + Block, Block), "rewatching did not return exactly its block to the write watch"); - Require(UnwatchSerial() != unwatched, "rewatching did not invalidate what was cached while the block was unwatched"); + Require(UnwatchSerial() != partly, "rewatching did not invalidate what was cached while the block was unwatched"); Require(UntrackedHint(imported, Block) == 0, "a rewatched block still reads as untracked"); Require(!UnchangedSince(imported, Block, before) && StoredOver(imported, Block, before), "a block returned to the write watch reads as unchanged since before it left"); Require(!UnchangedSinceCollected(imported, Block, before), "a CPU store made while the block was unwatched reads as no CPU store"); @@ -736,6 +742,125 @@ void CheckRewatch() { GuestArena::GuestArenaRelease_nid_postfix(memory, 4 * Block); } +using RewatchSpan = std::array, 1>; + +RewatchSpan BlockSpan(std::uint64_t address, std::size_t bytes) { + return {{{address & ~std::uint64_t{Block - 1}, (address + bytes + Block - 1) & ~std::uint64_t{Block - 1}}}}; +} + +RewatchSpan PageSpan(std::uint64_t address, std::size_t bytes) { + return {{{address & ~std::uint64_t{4095}, (address + bytes + 4095) & ~std::uint64_t{4095}}}}; +} + +void PrintPageState(const char* page, const void* address) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(address, &info, sizeof(info)) == sizeof(info), "cannot query a page next to an import"); + std::cout << page << ": State 0x" << std::hex << info.State << " Type 0x" << info.Type << " Protect 0x" << info.Protect << std::dec << '\n'; +} + +struct ImportFixture { + std::uint8_t* memory; + std::size_t reserved; + ImportFixture(std::size_t reservedBytes, std::size_t committed) : memory(static_cast(GuestArena::GuestArenaAllocate_nid_postfix(reservedBytes, Block))), reserved(reservedBytes) { + Require(memory != nullptr, "cannot reserve an import fixture"); + GuestArena::GuestArenaCommit_nid_postfix(memory, committed, PAGE_READWRITE, committed); + std::memset(memory, 0x11, committed); + BumpCollectEpoch(); + Require(CollectWrites(Base(), committed) != 0, "an import fixture was not collected"); + } + ImportFixture(const ImportFixture&) = delete; + ImportFixture& operator=(const ImportFixture&) = delete; + ~ImportFixture() { + GuestArena::GuestArenaReset_nid_postfix(memory, reserved); + GuestArena::GuestArenaRelease_nid_postfix(memory, reserved); + } + std::uint64_t Base() const { + return reinterpret_cast(memory); + } +}; + +void UnwatchImport(std::uint64_t address, std::size_t bytes) { + Require(ImportWatched(address, bytes, [] { return true; }, false) && !Watched(address, bytes), "an import that leaves the watch stayed watched"); +} + +void RequireRewatchedStores(std::uint64_t address, std::size_t bytes) { + BumpCollectEpoch(); + const auto collected = CollectWrites(address, bytes); + Require(collected != 0 && UnchangedSince(address, bytes, collected), "a rewatched import is not collected"); + *reinterpret_cast(address + bytes - 1) = 0x22; + CollectWritesUncached(address, bytes); + Require(!UnchangedSince(address + bytes - 1, 1, collected), "a CPU store into a rewatched import's last page was missed"); +} + +void CheckRewatchWholeBlockImport() { + constexpr std::size_t bytes = 4 * Block; + ImportFixture fixture(6 * Block, 6 * Block); + const auto import = fixture.Base() + Block; + const auto before = CollectWrites(fixture.Base(), 6 * Block); + UnwatchImport(import, bytes); + Require(PageSpan(import, bytes) == BlockSpan(import, bytes), "a whole-block import has a page span unlike its block span"); + Rewatch(PageSpan(import, bytes)); + Require(Watched(import, bytes), "a settled whole-block import did not return to the write watch"); + Require(!UnchangedSince(import, bytes, before) && UnchangedSince(fixture.Base(), Block, before) && UnchangedSince(import + bytes, Block, before), "rewatching a whole-block import did not stamp exactly its blocks"); + RequireRewatchedStores(import, bytes); +} + +void CheckRewatchImportTail() { + constexpr std::size_t bytes = 0xc4000; + ImportFixture fixture(13 * Block, bytes); + const auto import = fixture.Base(); + UnwatchImport(import, bytes); + PrintPageState("page past a 0xc4000-byte import", fixture.memory + bytes); + const auto unwatched = UnwatchSerial(); + Rewatch(BlockSpan(import, bytes)); + Require(!Watched(import, bytes) && UnwatchSerial() == unwatched, "the 64 KiB span past the import's committed pages returned to the write watch"); + Rewatch(PageSpan(import, bytes)); + Require(Watched(import, bytes), "a settled import whose size is not a whole block did not return to the write watch"); + Require(Watched(import + 12 * Block, bytes - 12 * Block) && !Watched(import, bytes + 1) && !Watched(import + bytes - 1, 2) && !Watched(import + bytes, 4096), "rewatching an import ending inside a block did not return exactly its pages"); + Require(UnwatchSerial() != unwatched && UntrackedHint(import, bytes) == 0, "a rewatched import still reads as unwatched"); + RequireRewatchedStores(import, bytes); +} + +void CheckRewatchUnalignedImport() { + constexpr std::size_t offset = 0x1000; + constexpr std::size_t bytes = 0xc4000; + constexpr std::size_t committed = 0xc8000; + ImportFixture fixture(13 * Block, committed); + const auto import = fixture.Base() + offset; + UnwatchImport(import, bytes); + PrintPageState("page past an import at block offset 0x1000", fixture.memory + committed); + Rewatch(BlockSpan(import, bytes)); + Require(!Watched(import, bytes), "the 64 KiB span past the committed pages around an import returned to the write watch"); + Rewatch(PageSpan(import, bytes)); + Require(Watched(import, bytes), "a settled import that does not start on a block did not return to the write watch"); + Require(!Watched(fixture.Base(), offset) && !Watched(import + bytes, 4096), "rewatching an import returned pages outside it"); + RequireRewatchedStores(import, bytes); +} + +void CheckRewatchImportBesideUnwatchedImport() { + constexpr std::size_t bytes = 0xc4000; + constexpr std::size_t otherBytes = 0x1c000; + ImportFixture fixture(14 * Block, 14 * Block); + const auto import = fixture.Base(); + const auto other = import + bytes; + UnwatchImport(import, bytes); + UnwatchImport(other, otherBytes); + Rewatch(PageSpan(import, bytes)); + Require(Watched(import, bytes), "an import sharing its last block with another unwatched import did not return to the write watch"); + Require(!Watched(other, 4096) && !Watched(import + 13 * Block - 4096, 4096) && !Watched(other + otherBytes - 4096, 4096), "rewatching an import returned another unwatched import's pages in its last block"); + Require(UntrackedHint(other, otherBytes) != 0, "an unwatched import beside a rewatched one does not read as untracked"); + RequireRewatchedStores(import, bytes); + Rewatch(PageSpan(other, otherBytes)); + Require(Watched(import, bytes + otherBytes), "rewatching the second import did not return both to the write watch"); +} + +void CheckRewatchImportPages() { + CheckRewatchWholeBlockImport(); + CheckRewatchImportTail(); + CheckRewatchUnalignedImport(); + CheckRewatchImportBesideUnwatchedImport(); +} + void CheckResidentDirectMappingReuse() { constexpr std::size_t bytes = 5 * Block; auto* memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block)); @@ -1532,6 +1657,10 @@ int main() { CheckResidentDirectMappingReuse(); return 0; } + if (std::wstring_view(GetCommandLineW()).ends_with(RewatchImportPagesMode)) { + CheckRewatchImportPages(); + return 0; + } #endif CheckSharedBlock(); CheckStoredOverUsesPageEvidence(); diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp index 62e56cfe52..121205b1a2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp @@ -99,6 +99,17 @@ int main() { coverage.Exclude(base + block, block, 43); require(coverage.Restore(base + block - page, 2 * page, 42), "a newer boundary page was not restored"); require(coverage.Covers(base + block - page, page) && !coverage.Covers(base + block, page), "a stale boundary mapping erased a newer neighbouring exclusion"); + constexpr std::size_t tail = 0xc4000; + coverage.Initialize(base, 14 * block); + coverage.Exclude(base, tail, 50); + require(coverage.Restore(base, tail, 51), "an import ending inside a block did not restore its pages"); + require(coverage.Covers(base, tail) && coverage.Covers(base + 12 * block, tail - 12 * block), "an import ending inside a block is not watched"); + require(!coverage.Covers(base, tail + 1) && !coverage.Covers(base + tail - 1, 2) && !coverage.Covers(base + tail, page) && !coverage.Covers(base, 13 * block), "a range past an import ending inside a block is watched"); + require(coverage.Covers(base + tail - page, page) && !coverage.Covers(base + 13 * block - page, page) && coverage.Covers(base + 13 * block, block), "an import ending inside a block changed the pages after it"); + coverage.Initialize(base, 14 * block); + coverage.Exclude(base + page, tail, 60); + require(coverage.Restore(base + page, tail, 61) && coverage.Covers(base + page, tail), "an import starting and ending inside blocks is not watched"); + require(!coverage.Covers(base, page) && !coverage.Covers(base, tail) && !coverage.Covers(base + page + tail, page) && !coverage.Covers(base + page, tail + 1), "an import starting and ending inside blocks watched pages outside it"); std::cout << "write watch coverage passed\n"; return 0; } catch (const std::exception& error) { From 3e3808e55b18c7838a67e24e9ed4c15bad6382e1 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 23 Oct 2026 08:57:52 +0000 Subject: [PATCH 163/428] fix(agc): rewatch only a settled import's own pages rewatchSettledImports passed an import's 64 KiB-rounded span to Rewatch. For an import whose size is not a block multiple the span's last block holds pages past the allocation, the walk fails on them, and the import stayed compared for good. Pass the import's own pages; the shared-block and live storage image checks still use the block span. --- .../prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp | 3 ++- docs/dev/ARCHITECTURE.md | 2 +- docs/dev/debt/libSceAgcDriver.md | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 95369e66b0..02157e17a8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -868,6 +868,7 @@ bool importsStale(const Context& context, const HostImports& state); constexpr std::uint64_t settleFloorBytes = 0x100000; #ifdef _WIN32 constexpr std::uint64_t settleBlockBytes = 0x10000; +constexpr std::uint64_t settlePageBytes = 0x1000; constexpr auto settlePollPeriod = std::chrono::microseconds(100); std::atomic deferredSettles{std::getenv("APS5_NO_DEFERRED_SETTLES") == nullptr}; #endif @@ -1126,7 +1127,7 @@ void rewatchSettledImports(HostImports& state) { continue; } bases.push_back(base); - ranges.push_back(span.front()); + ranges.emplace_back(base & ~(settlePageBytes - 1), (base + import->second.bytes + settlePageBytes - 1) & ~(settlePageBytes - 1)); } state.settleReady.clear(); if (ranges.empty()) return; diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 387d084365..5195dc9261 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -145,7 +145,7 @@ flowchart LR - Guest memory reaches the GPU through host imports (`VK_EXT_external_memory_host`) where the driver accepts them, and through copies otherwise ([`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)). A registered range larger than the import window (64 MiB) is imported per window for buffer and fill accesses and whole for an address-based build. Live imports stay under a budget, with the least recently used import retired first (see TechnicalDebt, Host import windows and budget). - A guest protect or unmap that overlaps a plain host-pointer import retires the whole import before changing the guest range (`WaitForLeasesAndImports` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)). Later accesses import surviving registered ranges again; imports never keep the pages across a cut pinned in a trimmed state. `GuestAllocations::Range::backingSerial` distinguishes the original pages from replacements: untouched pieces of a split keep their serial, newly mapped or protected pieces get a fresh one, and every span passed to `RequireUnpinned` (including the other mappings of remapped direct memory and the resident runs touched by a 2 MiB unmap or remap) gets a fresh serial when the mutation ends. - A draw binds its vertex and index ranges in place from a host import, except with NVIDIA's Windows driver, which reads a host import of guest memory far slower than a buffer it allocated (Astro Bot at Crash Site on an RTX 5070 Ti: 164 against 45 ms of draws per frame), so they are copied there (`DrawInputsInPlaceUsable`, `Context::drawInputsInPlace`). `APS5_DRAW_INPUTS_IN_PLACE=1` binds them in place and `APS5_NO_DRAW_INPUTS_IN_PLACE=1` copies them on any device; the second wins. -- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until a GPU use of it completes (a read on the settle queue ends it even while main-queue work that used the import first is still running, and the first use of an earlier import of the same pages after that read marks nothing), so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, for imports of 1 MiB or more; the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. A smaller import is compared when it is made, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread. Its 4-byte read is queued instead: the next import lookup that holds `GpuMutex` while the imports match the registry (`settleUnwatchedImports`) records the queued reads into one batch on the main queue, with barriers ordering earlier and later writes around the reads. Submission does not wait on the CPU, and once the batch's fence has signalled a later lookup returns the import's whole 64 KiB blocks to the write watch (`GuestMemory::Rewatch` drops their marks and stamps every block as a CPU store), unless a live storage image overlaps them. Such an import stays compared: an image that took its generation before the stamp would take the stamp for a CPU store over the results it produces after it, and drop them. A settled import whose range had already left the watch returns the same way, and an import retired while its read is in flight is kept with that batch and freed once the batch has completed, without waiting for it; a guest unmap of its memory waits for the batch instead. `APS5_NO_DEFERRED_SETTLES` (any value) keeps every import under 1 MiB compared. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. On Linux the settle queue is made when the device imports dma-buf memory, and every dma-buf import of 1 MiB or more is settled right after it is made, for another reason: the kernel maps an import's pages for the GPU at the first submission that uses it, which took about 2 s for a 3 GiB import on RADV and held up the title's frame. The import's buffer is shared with the settle queue's family instead of aliased, since a sparse import is bound to many memory objects. +- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until a GPU use of it completes (a read on the settle queue ends it even while main-queue work that used the import first is still running, and the first use of an earlier import of the same pages after that read marks nothing), so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, for imports of 1 MiB or more; the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. A smaller import is compared when it is made, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread. Its 4-byte read is queued instead: the next import lookup that holds `GpuMutex` while the imports match the registry (`settleUnwatchedImports`) records the queued reads into one batch on the main queue, with barriers ordering earlier and later writes around the reads. Submission does not wait on the CPU, and once the batch's fence has signalled a later lookup returns the import's pages to the write watch (`GuestMemory::Rewatch` drops the marks of every 64 KiB block they touch and stamps those blocks as a CPU store; the rest of a block the import only partly covers stays unwatched), unless a live storage image overlaps those blocks. Such an import stays compared: an image that took its generation before the stamp would take the stamp for a CPU store over the results it produces after it, and drop them. A settled import whose range had already left the watch returns the same way, and an import retired while its read is in flight is kept with that batch and freed once the batch has completed, without waiting for it; a guest unmap of its memory waits for the batch instead. `APS5_NO_DEFERRED_SETTLES` (any value) keeps every import under 1 MiB compared. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. On Linux the settle queue is made when the device imports dma-buf memory, and every dma-buf import of 1 MiB or more is settled right after it is made, for another reason: the kernel maps an import's pages for the GPU at the first submission that uses it, which took about 2 s for a 3 GiB import on RADV and held up the title's frame. The import's buffer is shared with the settle queue's family instead of aliased, since a sparse import is bound to many memory objects. ### Log output diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 42c2528c6a..be7aecb9a4 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -117,8 +117,8 @@ - [`APS5_SKIP_REFUSED_DRAWS`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp) (libSceAgcDriver) drops a draw whose graphics stage the recompiler refuses instead of failing the queue, so the frame lacks that draw, and the stage is compiled and refused again at each such draw. Compute refusals still fail. - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending saves its bytes through the unwatch hook, which also runs when a failed collect takes a range out of the watch. A range that leaves the watch without the hook running over it (an import fallback that unwatched without saving) leaves its pending results without saved bytes; storing such results throws. A unit whose results were stored while its blocks were watched keeps only a generation; when its blocks leave the watch while nothing is pending, the hook saves nothing, and results that land in it afterwards have neither saved bytes nor a watch, so storing them throws too. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory when the import probe keeps the write watch and a settle queue exists: those of 1 MiB or more stay watched, and smaller ones return to the watch once their settle has completed, unless a storage image overlaps them (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. -- [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) whose protection an import resolves count as stored only when their bytes change. A same-value CPU store during that window cannot be distinguished from driver activity and does not win over pending results. On Windows, NVIDIA 617.42 marks every private MEM_WRITE_WATCH page after import until a GPU use completes. The driver behavior was measured on this driver; other drivers are unverified. Imports of at least 1 MiB wait for a four-byte read on the separate settle queue inside the window. Smaller imports stay compared until a deferred read completes; a later lookup returns whole blocks to write watching with a new CPU-store stamp. Blocks overlapping a live storage image, blocks of an import retired before rewatching, and imports without a settle queue stay compared. Shared-memory imports retain their separate-alias tracking. -- [Deferred host-import reads](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) use the main queue with barriers ordering earlier and later writes around the reads. Submission does not wait on the CPU. An initial multisampled DCC import needing a range whose import still awaits its read submits it if needed and waits under the GPU mutex (`WatchedAfterSettle`, #9724). Imports of partial 64 KiB blocks get no deferred read; only that path reads them, returning partial blocks whole as CPU stores unless a live, trimmed or parked compared import or storage image shares them; one a recorder holds is unseen. The latency cost of placing batches behind main-queue work is unmeasured. The first-use test now checks that earlier main-queue reads finish before the import returns to write watching; the original two-queue canary measured driver marking while those reads were still in flight. The earlier-import canary still checks later use of another import of the same pages. +- [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) whose protection an import resolves count as stored only when their bytes change. A same-value CPU store during that window cannot be distinguished from driver activity and does not win over pending results. On Windows, NVIDIA 617.42 marks every private MEM_WRITE_WATCH page after import until a GPU use completes. The driver behavior was measured on this driver; other drivers are unverified. Imports of at least 1 MiB wait for a four-byte read on the separate settle queue inside the window. Smaller imports stay compared until a deferred read completes; a later lookup returns their pages to write watching, stamping their blocks as CPU stores; the rest of a partly covered block stays compared. Blocks overlapping a live storage image, blocks of an import retired before rewatching, and imports without a settle queue stay compared. Shared-memory imports retain their separate-alias tracking. +- [Deferred host-import reads](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) use the main queue with barriers ordering earlier and later writes around the reads. Submission does not wait on the CPU. An initial multisampled DCC import needing a range whose import still awaits its read submits it if needed and waits under the GPU mutex (`WatchedAfterSettle`, #9724). Imports of partial 64 KiB blocks get no deferred read; only that path reads them, returning their pages as CPU stores unless a live, trimmed or parked compared import or storage image shares their blocks; one a recorder holds is unseen. The latency cost of placing batches behind main-queue work is unmeasured. The first-use test now checks that earlier main-queue reads finish before the import returns to write watching; the original two-queue canary measured driver marking while those reads were still in flight. The earlier-import canary still checks later use of another import of the same pages. - [Settled dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a dma-buf import of 1 MiB or more is read by a 4-byte copy on the settle queue right after it is made (`settleImport`), so the kernel maps its pages for the GPU there and not in the first submission that uses it (about 2 s for a 3 GiB import on RADV). The import's buffer is shared with the settle queue's family. An import made at map time settles on the guest thread that maps the range, inside its map call, so that thread waits while the kernel maps the pages: 0.47 to 2.2 s for the 3 GiB range of Hades (PPSA03355) and 0.49 to 4.3 s for the 4.5 GiB range of ASTRO BOT (PPSA21567) on RADV. Both titles map these ranges at start-up; a title that maps large ranges during gameplay stalls its mapping thread for as long. An import made from a submission settles under the GPU mutex, so its frame still pays for it; a smaller import, and every import on a device with no settle queue (one without a copy-capable queue besides the main one), pays in its first submission. A range imported through a host pointer, as lavapipe imports shared memory, is not settled - [Sparse dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a shared-backed range larger than one 64 MiB udmabuf buffer is imported as one sparse buffer bound to the 64 MiB buffers, on a queue of its own when the device has a second family that binds sparse memory. The path is decided before anything is imported, and a failure after that throws. A range whose offset in the backing file is not a multiple of 64 KiB starts its sparse buffer at the 64 KiB block below it (`HostImport::lead`). A range takes the single udmabuf import instead, which udmabuf's `size_limit_mb` refuses above 64 MiB by default so the range is copied, when the buffer's sparse block size is larger than 64 KiB (it is 64 KiB on RADV), when the sparse buffer does not accept every memory type a chunk import can have, when the backing file ends inside its last chunk, or when it is imported at map time on a device with no second family (the bind then needs the GPU mutex); the NVIDIA proprietary driver imports no dma-buf memory - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. From 13ebd7d43cb038698a79d066ef50d6ef624b38a7 Mon Sep 17 00:00:00 2001 From: Oscar45397 <238734290+Oscar45397@users.noreply.github.com> Date: Sat, 24 Oct 2026 01:40:22 +0200 Subject: [PATCH 164/428] fix(tests): retry the Windows working directory read until the child has written it Co-Authored-By: Claude Opus 5.5 --- .../relinker/tests/test_windows_working_directory.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/relinker/relinker/tests/test_windows_working_directory.py b/core/relinker/relinker/tests/test_windows_working_directory.py index c3cc6bf964..2692c59082 100644 --- a/core/relinker/relinker/tests/test_windows_working_directory.py +++ b/core/relinker/relinker/tests/test_windows_working_directory.py @@ -50,7 +50,10 @@ def current_directory(pid): parameters = struct.unpack(' Date: Sat, 24 Oct 2026 02:58:00 +0300 Subject: [PATCH 165/428] fix(libkernel_sys): resolve linkat paths relative to a directory descriptor --- core/libs/prx/libkernel_sys/Export.cpp | 38 ++++++++++++++++++-------- core/libs/tests/GuestKernelSys.cpp | 16 +++++++++-- docs/dev/debt/libkernel_sys.md | 2 +- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 3a5bda20b0..048d457629 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -436,12 +436,7 @@ static int FollowLinkPath(const std::filesystem::path& source, std::filesystem:: } #endif -int APS5_VABI link_nid_postfix(const char* existing, const char* created) { - if (existing == nullptr || created == nullptr) return static_cast(Fail(GuestEfault)); - if (*existing == '\0' || *created == '\0') return static_cast(Fail(GuestEnoent)); - std::filesystem::path source, destination; - if (const int error = ResolvePathError_nid_no_patch(existing, source)) return static_cast(Fail(error)); - if (const int error = ResolvePathError_nid_no_patch(created, destination)) return static_cast(Fail(error)); +static int LinkNative(const std::filesystem::path& source, const std::filesystem::path& destination) { std::error_code error; const auto status = std::filesystem::status(source, error); if (!std::filesystem::exists(status)) { @@ -468,15 +463,34 @@ int APS5_VABI link_nid_postfix(const char* existing, const char* created) { return 0; } +int APS5_VABI link_nid_postfix(const char* existing, const char* created) { + if (existing == nullptr || created == nullptr) return static_cast(Fail(GuestEfault)); + if (*existing == '\0' || *created == '\0') return static_cast(Fail(GuestEnoent)); + std::filesystem::path source, destination; + if (const int error = ResolvePathError_nid_no_patch(existing, source)) return static_cast(Fail(error)); + if (const int error = ResolvePathError_nid_no_patch(created, destination)) return static_cast(Fail(error)); + return LinkNative(source, destination); +} + +static int ResolveAtPath(int fd, const char* path, std::filesystem::path& native) { + if (fd == GuestAtFdcwd || *path == '/') return ResolvePathError_nid_no_patch(path, native); + if (std::strlen(path) >= GuestPathMax || HasLongComponent(path)) return GuestEnametoolong; + int error = 0; + const char* base = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); + if (base == nullptr) return error; + native = std::filesystem::path(reinterpret_cast(base)) / std::filesystem::path(reinterpret_cast(path)); + return 0; +} + int APS5_VABI linkat_nid_postfix(int fd1, const char* path1, int fd2, const char* path2, int flag) { if (flag & ~GuestAtSymlinkFollow) return static_cast(Fail(GuestEinval)); if (path1 == nullptr || path2 == nullptr) return static_cast(Fail(GuestEfault)); - if ((fd1 != GuestAtFdcwd && *path1 != '/') || (fd2 != GuestAtFdcwd && *path2 != '/')) - NotImplemented_nid_no_patch("linkat: a relative path with a directory descriptor"); - std::filesystem::path source; - if (flag == 0 && *path1 != '\0' && ResolvePathError_nid_no_patch(path1, source) == 0 && IsSymbolicLink(source)) - NotImplemented_nid_no_patch("linkat: a hard link to a symbolic link itself"); - return link_nid_postfix(path1, path2); + if (*path1 == '\0' || *path2 == '\0') return static_cast(Fail(GuestEnoent)); + std::filesystem::path source, destination; + if (const int error = ResolveAtPath(fd1, path1, source)) return static_cast(Fail(error)); + if (const int error = ResolveAtPath(fd2, path2, destination)) return static_cast(Fail(error)); + if (flag == 0 && IsSymbolicLink(source)) NotImplemented_nid_no_patch("linkat: a hard link to a symbolic link itself"); + return LinkNative(source, destination); } int APS5_VABI fchdir_nid_postfix(int fd) { diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 43657d062c..4f5aaa08a9 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -163,8 +163,20 @@ void OwnershipAndLinkTests(const std::filesystem::path& root) { Require(linkat_nid_postfix(-100, file.c_str(), -100, other.c_str(), 0x200) == -1 && Error() == 22); Require(linkat_nid_postfix(-100, nullptr, -100, other.c_str(), 0) == -1 && Error() == 14); Require(linkat_nid_postfix(-100, "", -100, other.c_str(), 0) == -1 && Error() == 2); - Require(ThrowsNaming([&] { linkat_nid_postfix(directory, "owned.txt", -100, other.c_str(), 0); }, "linkat: a relative path")); - Require(ThrowsNaming([&] { linkat_nid_postfix(-100, file.c_str(), directory, "other.txt", 0); }, "linkat: a relative path")); + Require(linkat_nid_postfix(directory, "owned.txt", -100, other.c_str(), 0) == 0); + Require(std::filesystem::hard_link_count(file) == 3 && std::filesystem::remove(other)); + Require(linkat_nid_postfix(-100, file.c_str(), directory, "other.txt", 0) == 0); + Require(std::filesystem::hard_link_count(file) == 3); + Require(linkat_nid_postfix(directory, "owned.txt", directory, "other.txt", 0) == -1 && Error() == 17); + Require(linkat_nid_postfix(directory, "missing.txt", directory, "absent.txt", 0) == -1 && Error() == 2); + Require(linkat_nid_postfix(4321, "owned.txt", directory, "absent.txt", 0) == -1 && Error() == 9); + Require(linkat_nid_postfix(directory, "owned.txt", 4321, "absent.txt", 0) == -1 && Error() == 9); + const int plain = sceKernelOpen(file.c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(plain >= 0); + Require(linkat_nid_postfix(plain, "owned.txt", directory, "absent.txt", 0) == -1 && Error() == 20); + Require(linkat_nid_postfix(directory, "owned.txt", plain, "absent.txt", 0) == -1 && Error() == 20); + Require(sceKernelClose(plain) == 0 && std::filesystem::remove(other)); + Require(!std::filesystem::exists(root / "absent.txt")); const auto symbolic = (root / "owned.link").string(); const auto dangling = (root / "dangling.link").string(); if (symlink_nid_postfix("owned.txt", symbolic.c_str()) == 0) { diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 02c6230564..d9c789e529 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -29,7 +29,7 @@ - [chroot](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L776-L807) and [`pwd_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L3059-L3085) for a root caller: `EFAULT` for a null path, `ENOENT` for an empty or missing one, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255, and `ENOTDIR` for a path that is not a directory or passes through a file. A directory that is the guest root (`/`, `/..`, or `.` after `fchdir` to a descriptor of `/`) succeeds and changes nothing, as the root stays the same directory; the guest is taken never to have changed its root, so open directory descriptors do not make it fail with `EPERM` (`kern.chroot_allow_open_directories` 1). Any other directory throws, naming the path: the guest path resolver has a fixed root and mount aliases (`/app0`, `/savedata0`) that do not move with it. On the console a title starts in a sandbox root, where `pwd_chroot` refuses with `EPERM` while a directory is open; whether that holds for titles, and whether a title holds `PRIV_VFS_CHROOT`, are unverified. Porpoise (PPSA99764) weak-imports it and calls it only as `fchdir` to its saved descriptor of `/` then `chroot(".")`, to put its sandbox root back after a jailbreak daemon replaced it ([`put_root_back`](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5/blob/2ed1ab5a06995aa198f7bccd4da0f90c3fc29d39/src/porpoise_jailbreak.cpp#L303-L331)) - [fchown](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled: the guest's own owner and group (0) and the unchanged value (-1) are accepted, a descriptor that is not open fails with `EBADF`, and any other owner or group throws, naming it. The check reads the descriptor with `fstat`, ignoring the CRT's invalid-parameter handler on Windows as `DescriptorControl` does. The console's ownership rules are unverified. Morrowind (`mshivam019/morrowind-ps5`) imports it - [chown, lchown and fchownat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled, as for `fchown`: owner and group 0 and the unchanged value (-1) are accepted, and any other owner or group throws, naming it. The path is looked up first, as in FreeBSD 11's `kern_fchownat`, with `stat`, with `lstat` for `lchown`, and with `fstatat` for `fchownat`, so a lookup error is returned before the owner is checked and nothing is changed on the host. `fchownat` flags other than `AT_SYMLINK_NOFOLLOW` (0x200) return `EINVAL`, as in FreeBSD 11. The console's ownership rules are unverified -- [linkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `link`, and flags other than `AT_SYMLINK_FOLLOW` (0x400) return `EINVAL`, as in FreeBSD 11. A relative path with a directory descriptor throws, and so does a symbolic link as the source without `AT_SYMLINK_FOLLOW`, where FreeBSD links the symbolic link itself. Console behaviour is unverified +- [linkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) resolves `AT_FDCWD` (-100) and absolute paths as `link` does, and flags other than `AT_SYMLINK_FOLLOW` (0x400) return `EINVAL`, as in FreeBSD 11. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. A symbolic link as the source without `AT_SYMLINK_FOLLOW` throws, where FreeBSD links the symbolic link itself. Console behaviour is unverified - [pthread_set_defaultstacksize_np and scePthreadSetDefaultstacksize](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) set the stack size that `scePthreadAttrInit` and a thread created without attributes use, as [fpPS4's `pthread_set_defaultstacksize_np`](https://github.com/red-prig/fpPS4/blob/04cefd4/kernel/ps4_pthread_attr.pas#L89-L102) does: a size of 16384 bytes or less fails with `EINVAL` (`0x80020016` from the sce form) and there is no upper limit, so a default the host cannot allocate fails only when a thread is created. Attributes initialised earlier keep their size. Not checked on a PS5 - [sceKernelTitleWorkaroundIsEnabled](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - not measured on a console; the `{version, align, ids[2]}` layout, the bit test and the limit of 0x3A bits follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/kernel/kernel.cpp), which notes that the limit changes between firmwares. A negative bit is `EINVAL` here; shadPS4 compares it signed and reads outside `ids`. Titles usually pass the `title_workaround` block of `sceKernelGetAppInfo`, which does not write that block here, so the bits read are whatever the caller's memory held - [sceKernelGetProcessType](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - not measured on a console; as in shadPS4, the title's own pid, -1 and 0 are type 0 (big application) and any other pid is `ESRCH` From b3078134383df25df9972b0f2f29ea9d21699d11 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sat, 24 Oct 2026 02:06:01 +0200 Subject: [PATCH 166/428] test(libSceNgs2): pin the PS5 pan codes, NaN LFE level and radian wrap throw The tests now check the SDK values of the pan error constants, that 0 params leaves the output untouched, that a NaN param angle keeps its LFE level with 7 speakers, and that a param one float below the first speaker in radians throws. The throw messages and the debt entry say the PS5 module did not return within 60 s for those inputs instead of claiming it never returns. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceNgs2.native/src/Pan.cpp | 8 +++---- .../libSceNgs2.native/tests/Ngs2Render.cpp | 22 +++++++++++++++++-- docs/dev/debt/libSceNgs2.native.md | 2 +- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Pan.cpp b/core/libs/prx/libSceNgs2.native/src/Pan.cpp index b41ca734ff..f59c436bc9 100644 --- a/core/libs/prx/libSceNgs2.native/src/Pan.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Pan.cpp @@ -77,12 +77,12 @@ int Ngs2PanInit(Ngs2PanWork* work, const float* speaker_angles, float unit_angle for (std::uint32_t i = 0; i < num_speakers; i++) { float angle = speaker_angles[layout->inputOrder[i]]; while (angle < 0.0f) { - if (angle + unit_angle == angle) throw std::runtime_error("NGS2: pan speaker angles too large to wrap are not implemented (the PS5 library never returns for them)"); + if (angle + unit_angle == angle) throw std::runtime_error("NGS2: pan speaker angles too large to wrap are not implemented (the PS5 library did not return for them within 60 s)"); angle += unit_angle; } while (angle >= unit_angle) { - if (!(unit_angle < angle)) throw std::runtime_error("NGS2: a pan speaker angle that wraps to the unit angle is not implemented (the PS5 library never returns for it)"); - if (angle - unit_angle == angle) throw std::runtime_error("NGS2: pan speaker angles too large to wrap are not implemented (the PS5 library never returns for them)"); + if (!(unit_angle < angle)) throw std::runtime_error("NGS2: a pan speaker angle that wraps to the unit angle is not implemented (the PS5 library did not return for it within 60 s)"); + if (angle - unit_angle == angle) throw std::runtime_error("NGS2: pan speaker angles too large to wrap are not implemented (the PS5 library did not return for them within 60 s)"); angle -= unit_angle; } work->speaker_angles[i] = angle; @@ -116,7 +116,7 @@ int Ngs2PanGetVolumeMatrix(Ngs2PanWork* work, const Ngs2PanParam* params, uint32 const float before = angle; if (angle >= first + unit) angle -= unit; if (angle < first) angle += unit; - if (angle == before) throw std::runtime_error("NGS2: pan angles that do not wrap into one turn are not implemented (the PS5 library never returns for them)"); + if (angle == before) throw std::runtime_error("NGS2: pan angles that do not wrap into one turn are not implemented (the PS5 library did not return for them within 60 s)"); } float opposite = angle + unit * 0.5f; if (!(opposite < first + unit)) opposite -= unit; diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index 3aff39060e..7dd9254064 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -239,10 +239,13 @@ static void TestPan() { } const Ngs2PanParam front{0.0f, 1.0f, 1.0f, 0.0f}; - float out[2] = {}; + float out[2] = {-1.0f, -1.0f}; Require(sceNgs2PanInit(&work, nullptr, 360.0f, 2) == SCE_NGS2_OK); Require(sceNgs2PanGetVolumeMatrix(&work, &front, 1, 2, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); - Require(sceNgs2PanGetVolumeMatrix(&work, &front, 0, 2, out) == SCE_NGS2_OK); + Require(sceNgs2PanGetVolumeMatrix(&work, &front, 0, 2, out) == SCE_NGS2_OK && out[0] == -1.0f && out[1] == -1.0f); + Require(SCE_NGS2_ERROR_INVALID_PAN_UNIT_ANGLE == static_cast(0x804A8450) && SCE_NGS2_ERROR_INVALID_PAN_SPEAKER == static_cast(0x804A8451)); + Require(SCE_NGS2_ERROR_INVALID_PAN_MATRIX_FORMAT == static_cast(0x804A8452) && SCE_NGS2_ERROR_INVALID_PAN_WORK == static_cast(0x804A8453)); + Require(SCE_NGS2_ERROR_INVALID_PAN_PARAM == static_cast(0x804A8454)); float guard[32]; const auto panResult = [&](Ngs2PanWork* panWork, const Ngs2PanParam* params, std::uint32_t numParams, std::uint32_t format) { std::fill(std::begin(guard), std::end(guard), -1.0f); @@ -273,6 +276,21 @@ static void TestPan() { const Ngs2PanParam laterNan[2] = {front, nanAngle}; Require(panResult(&work, laterNan, 2, 2) == SCE_NGS2_OK); Require(std::fabs(guard[0] - 0.707106769f) < 1e-6f && guard[1] == 0.0f && std::fabs(guard[2] - 0.707106769f) < 1e-6f && guard[3] == 0.0f); + Ngs2PanWork sevenWork{}; + Require(sceNgs2PanInit(&sevenWork, nullptr, 360.0f, 7) == SCE_NGS2_OK); + const Ngs2PanParam nanEach[4] = {{NAN, 1.0f, 1.0f, 0.5f}, {30.0f, NAN, 1.0f, 0.5f}, {30.0f, 1.0f, NAN, 0.5f}, {30.0f, 1.0f, 1.0f, NAN}}; + Require(panResult(&sevenWork, nanEach, 4, 8) == SCE_NGS2_OK); + for (std::uint32_t channel = 0; channel < 8; channel++) { + Require(guard[channel * 4] == (channel == 3 ? 0.5f : 0.0f)); + for (std::uint32_t p = 1; p < 4; p++) Require(std::isnan(guard[channel * 4 + p])); + } + Ngs2PanWork radianWork{}; + const float radianPair[] = {-0.52359879f, 0.52359879f}; + Require(sceNgs2PanInit(&radianWork, radianPair, 2.0f * pi, 2) == SCE_NGS2_OK && radianWork.speaker_angles[0] == 0.52359879f); + const Ngs2PanParam belowFirst{0.523598731f, 1.0f, 1.0f, 0.0f}; + const Ngs2PanParam aboveFirst{0.52359885f, 1.0f, 1.0f, 0.0f}; + Require(std::nextafter(0.52359879f, 0.0f) == belowFirst.angle && PanThrows(&radianWork, &belowFirst, 1, 1)); + Require(panResult(&radianWork, &aboveFirst, 1, 1) == SCE_NGS2_OK && std::fabs(guard[0] - 0.707106769f) < 1e-6f); const Ngs2PanParam huge{1e30f, 1.0f, 1.0f, 0.0f}; Require(PanThrows(&work, &huge, 1, 2)); const float hugeSpeaker[] = {1e30f, 30.0f}; diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 1a55fbed72..a8d5efeb43 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -44,7 +44,7 @@ - [sceNgs2VoiceControl patch](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a destination rack handle with input id 0 resolves to that rack's first voice; this matches the Dead Cells call in #4334, but selection for racks with multiple voices has not been checked against a console. Nonzero destination input ids still throw. `sceNgs2VoiceGetPortInfo` reports the resolved voice handle and zero delay. - [sceNgs2ParseWaveformFile](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw - [sceNgs2VoiceControl sampler filter](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown -- [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - inputs on which the PS5 library never returns throw: an infinite speaker or param angle, an angle too large for a turn to change it, a speaker angle that wraps to exactly the unit angle, and a param angle that wraps onto the first speaker plus one turn, such as one float below the first speaker in radians. Everything else matches the PS5 `libSceNgs2.native.sprx` of firmware 3.20 and 4.03 bit for bit, using its own `sinf`: 2, 4, 5 and 7 speakers, default and explicit angles in degrees and radians, matrix formats 1, 2, 6 and 8, distance and level sweeps and lists of 8 params. Speakers stay in the fixed input order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb, not sorted, and radian defaults keep the wrap-around entry in degrees, as on PS5 +- [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - inputs on which the PS5 `libSceNgs2.native.sprx` of firmware 3.20 and 4.03 did not return within 60 s throw; whether it returns at all for them is unknown. They are an infinite speaker or param angle, an angle too large for a turn to change it, a speaker angle that wraps to exactly the unit angle, and a param angle that wraps onto the first speaker plus one turn, such as one float below the first speaker in radians - [sceNgs2GeomResetListenerParam, sceNgs2GeomResetSourceParam](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) (libSceNgs2.native) - the structures (12, 16, 16, 60, 96, 108, 32 and 308 bytes) come from [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `ngs2.cpp` and match [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_geom.h` field for field, but only KytyPS5 gives the behaviour - shadPS4 logs and returns success without writing the output. The defaults (orient front +Z, orient up +Y, sound speed 343, cone 1/360/1/360, rolloff 0/1000000/1/1, doppler factor, full-band, LFE and maximum level 1, two speakers and matrix format 2) are unverified on a PS5 title; KytyPS5 and iStark/PS5PCEM agree on them. PPSA15552 (Dead Cells) calls `sceNgs2GeomResetSourceParam` after creating its racks. A null output address returns `INVALID_OUT_ADDRESS` as elsewhere in the library rather than throwing as KytyPS5 does. `sceNgs2GeomApply` is now a minimal pass-through that follows [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `Ngs2GeomApply` (shadPS4 only stubs it): pitch 1, source position, volume the larger of the source minimum and maximum level, and a diagonal of the source maximum level (1 when it is not positive) over `matrix_format` channels (0 is two, at most 8). The listener and `flags` are ignored, so source attenuation, distance rolloff, panning and Doppler stay unimplemented; neither KytyPS5 nor this has been measured on a PS5, and a null output address returns `INVALID_OUT_ADDRESS` where KytyPS5 exits - [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) builds an orthonormal world-to-listener transform. The row-major matrix, front-axis priority and flag 1 for right-handed input follow [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/src/hle/ngs2_geom.zig); they have not been measured on PS5 hardware. Velocity stays in world coordinates and positive finite sound speed is preserved. Orientation vectors are normalised and up is made perpendicular to front, so non-unit and slightly tilted vectors are accepted. When front and up are parallel, up is replaced by `(0,1,0)` or `(1,0,0)` so the basis stays defined: PPSA15552 (Dead Cells) passes `orient_front={0,0,1}` with `orient_up={0,0,1}` after moving the listener, which PS5PCEM rejects; the console error code is unknown. Zero orientation vectors, nonfinite input and an unrepresentable translation still throw without changing the output; console error codes are unknown, and a nonpositive sound speed throws where PS5PCEM and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/ngs2.cpp) default it to 343. KytyPS5 always writes the identity matrix, so the transform rests on PS5PCEM plus that Dead Cells case. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334. This does not implement source attenuation, panning or Doppler in `sceNgs2GeomApply`. - [NGS2 custom sampler rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4001) - implements bounded support for the custom sampler rack and its built-in module 0x13 (Custom Voice Filter), unblocking PPSA15552 (Dead Cells) startup. Structure layouts come from [shadPS4's ngs2_custom.h](https://raw.githubusercontent.com/shadps4-emu/shadPS4/main/src/core/libraries/ngs2/ngs2_custom.h) (OrbisNgs2CustomVoiceFilterParam, 40 bytes) and live trace correlation from [prosper's hle_audio.cpp](https://raw.githubusercontent.com/mattias800/prosper/main/prosper/src/hle/audio/hle_audio.cpp) (0x40001300 command and parameters like 10000.0f and 0.707f). Routes parameter updates via the 0x40001300 command family into the Biquad filter engine. Filter type 0 clears the destination filter; type 1 configures the RBJ low-pass biquad frequency, Q and level. Other filter types and unmodeled configurations throw. From 9c3ffcf5b00fbc760cadf6e431fd71b17bc56182 Mon Sep 17 00:00:00 2001 From: SuarezPM Date: Fri, 23 Oct 2026 21:14:39 -0300 Subject: [PATCH 167/428] feat(shader): support 64-bit image atomics through keyed image tables Table T#s keyed at runtime feeding image_atomic64_* now lower to OpAtomic* on the table Atomic64View instead of faulting, mirroring the 32-bit path: gate, dedup identity, binding flags and view selection. Packed formats and BY element counts still fault; hosts need VK_FORMAT_FEATURE_STORAGE_IMAGE_ATOMIC_BIT for the view format. --- .../Graphics/src/ShaderResources.cpp | 4 +-- .../libSceAgcDriver/tests/ShaderMemory.cpp | 15 +++++++- .../tests/execution/ImageTable.cpp | 35 +++++++++++++++++++ .../Optimization/ResourceMaterializer.hpp | 1 + .../src/DescriptorBindingBuilder.cpp | 1 + .../Optimization/src/ResourceMaterializer.cpp | 6 ++-- .../Optimization/src/ResourceTracker.cpp | 2 +- docs/dev/debt/shader.md | 2 +- 8 files changed, 58 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index e437e9d1d5..244fc1eff6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2445,7 +2445,7 @@ void ShaderResources::buildComplete() { for (auto element = first; element < last; ++element) { const auto index = binding.imageAllocations[element]; if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_SAMPLER) images.push_back({samplers[index]->Handle(), VK_NULL_HANDLE, VK_IMAGE_LAYOUT_UNDEFINED}); - else if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE) images.push_back({VK_NULL_HANDLE, storageAtomic[index] ? storageTextures[index]->AtomicView(storageMips[index], storageFirstLayer[index]) : storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index]), VK_IMAGE_LAYOUT_GENERAL}); + else if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE) images.push_back({VK_NULL_HANDLE, storageAtomic64[index] ? storageTextures[index]->Atomic64View(storageMips[index], storageFirstLayer[index]) : storageAtomic[index] ? storageTextures[index]->AtomicView(storageMips[index], storageFirstLayer[index]) : storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index]), VK_IMAGE_LAYOUT_GENERAL}); else images.push_back({VK_NULL_HANDLE, textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()}); } if (recorded) runs.emplace_back(write.dstArrayElement, write.descriptorCount); @@ -5227,7 +5227,7 @@ void ShaderResources::resolveImageTables() { storageFirstLayer.push_back(firstLayer); storageWritten.push_back(true); storageAtomic.push_back(element < binding.imageAtomic.size() && binding.imageAtomic[element]); - storageAtomic64.push_back(false); + storageAtomic64.push_back(element < binding.imageAtomic64.size() && binding.imageAtomic64[element]); describedRanges.push_back({"storage", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress, true}); bindings[storageBinding].imageAllocations[absolute] = storageTextures.size() - 1u; continue; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index bb480c295f..c21a0a8aed 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3266,6 +3266,19 @@ void verifyImageTableRegistration() { const auto* atomicTable = TableBinding(*atomic.result, DescriptorRole::ImageStorageTable); require((atomicFault & Abi::PoisonFlag) == 0u && atomicTable != nullptr && atomicTable->count != 0u && atomicTable->imageAtomic.size() == atomicTable->count && std::all_of(atomicTable->imageWritten.begin(), atomicTable->imageWritten.end(), [](bool written) { return written; }) && std::all_of(atomicTable->imageAtomic.begin(), atomicTable->imageAtomic.end(), [](bool atomic) { return atomic; }), "image table registration: the image_atomic_add table was not bound as atomic storage elements"); require(TableBinding(*atomic.result, DescriptorRole::ImageTable) == nullptr || TableBinding(*atomic.result, DescriptorRole::ImageTable)->count == 0u, "image table registration: an atomic-only table bound sampled elements"); + auto atomic64Code = storeCode; + atomic64Code[8] = 0xf0440308u; + guest.heap[0] = TableT(texture.bytes.data(), TableFormat32_32UInt, TableType2D); + static constexpr std::array int64ImageCapabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityInt64ImageEXT}; + auto atomic64Request = TableRequest(atomic64Code, userData); + atomic64Request.target.supportedCapabilities = int64ImageCapabilities; + const auto atomic64 = CompileTable(atomic64Request); + const auto& atomic64Plan = *atomic64.capture->plan; + require(atomic64Plan.info.images.size() == 1u && atomic64Plan.info.images[0].table == 0u && atomic64Plan.info.images[0].tableOperation == TableOperation::Supported, "image table registration: a table T# feeding image_atomic64_add was not a supported table operation"); + const auto& atomic64Map = TableMap(*atomic64.result); + const auto atomic64Fault = atomic64Map[Abi::TableHeader(0) + Abi::TableFault]; + const auto* atomic64Table = TableBinding(*atomic64.result, DescriptorRole::ImageStorageTable); + require((atomic64Fault & Abi::PoisonFlag) == 0u && atomic64Table != nullptr && atomic64Table->count != 0u && atomic64Table->imageAtomic64.size() == atomic64Table->count && std::all_of(atomic64Table->imageAtomic64.begin(), atomic64Table->imageAtomic64.end(), [](bool atomic) { return atomic; }), "image table registration: the image_atomic64_add table was not bound as 64-bit atomic storage elements"); const std::array, 2> unsupportedStores{{{0xf0240f08u, "image_store_mip"}, {0xf0280f08u, "image_store_pck"}}}; for (const auto& [instruction, name] : unsupportedStores) { auto code = storeCode; @@ -3684,7 +3697,7 @@ void verifySelectedRecordDirectOperations() { require(word0 == 0xf0440308u || (TableBinding(*compiled.result, DescriptorRole::ImageTable) == nullptr && compiled.result->imageTableFaults == 0u && TableCovered(compiled.regions, &heap[2], sizeof(heap[2]))), (what + " through a T# whose record the CPU evaluates was not bound from its record").c_str()); const auto wholeCode = WithImageOperation(TableWholeCode, word0, false); const auto whole = GetResourcePlan(TableRequest(wholeCode, userData)); - require(whole->info.images.size() == 1u && whole->info.images[0].table == 0u && whole->info.images[0].tableOperation == (word0 == 0xf0200f08u || word0 == 0xf0440108u ? TableOperation::Supported : TableOperation::Unsupported), (what + " through a T# keyed per lane left the image table or has the wrong table operation").c_str()); + require(whole->info.images.size() == 1u && whole->info.images[0].table == 0u && whole->info.images[0].tableOperation == (word0 == 0xf0200f08u || word0 == 0xf0440108u || word0 == 0xf0440308u ? TableOperation::Supported : TableOperation::Unsupported), (what + " through a T# keyed per lane left the image table or has the wrong table operation").c_str()); } const auto mixedCode = WithImageOperation(TableSrtKeyCode, 0xf0200f08u, true); const auto mixed = GetResourcePlan(TableRequest(mixedCode, userData)); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index d5a89a1586..5ee8abca21 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -47,6 +47,7 @@ constexpr std::uint32_t MaxGroups = 8; constexpr std::uint32_t Format32UInt = 20; constexpr std::uint32_t Format32SInt = 21; constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t Format32_32UInt = 62; constexpr std::uint32_t Format11_11_10UInt = 34; constexpr std::uint32_t Format8_8Srgb = 129; constexpr std::uint32_t Type2D = 9; @@ -56,6 +57,7 @@ constexpr std::uint32_t DepthSwizzleMode = 0x18; alignas(256) std::array FloatTexels{}; alignas(256) std::array OtherFloatTexels{}; alignas(256) std::array UintTexels{}; +alignas(256) std::array Uint64Texels{}; alignas(256) std::array SintTexels{}; alignas(4096) std::array DepthStorage{}; alignas(256) std::array Table{}; @@ -208,6 +210,12 @@ alignas(256) constexpr std::array AtomicAddLaneTableCode{ 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7e080301u, 0xf0448108u, 0x00090401u, 0xbf810000u, }; +alignas(256) constexpr std::array AtomicAdd64LaneTableCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, + 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7e080301u, 0x7e0a02ffu, 0x00000000u, 0xf0448308u, + 0x00090401u, 0xbf810000u, +}; + alignas(256) constexpr std::array AtomicSwapLaneTableCode{ 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7e080301u, 0xf03c8108u, 0x00090401u, 0xbf810000u, @@ -1168,6 +1176,32 @@ void RunAtomicTests(AgcDriver::VulkanDevice& device) { for (std::uint32_t x = 0; x < Width; ++x) Require(UintTexels[x] == (x ^ 1u) && std::bit_cast(FloatTexels[x]) == 0x3f800000u + x, "image table atomic: an add through a null entry reached an image:\n" + null.log); } +void RunAtomic64Tests(AgcDriver::VulkanDevice& device) { + if (!TargetHasCapability(device.Target(), spv::CapabilityInt64ImageEXT)) { + std::puts("skipped the 64-bit table atomic rows, the device has no shaderImageInt64Atomics"); + return; + } + struct Registration { + void* data; + Registration(void* data, std::size_t bytes) : data(data) { GuestAllocations::Mutation().Add(data, bytes, true, true, true); } + ~Registration() { GuestAllocations::Mutation().Remove(data); } + } registered(Uint64Texels.data(), sizeof(Uint64Texels)); + SetClassRecords(); + for (std::uint32_t x = 0; x < Width; ++x) Uint64Texels[x] = 0x5000000000000000ull + x; + SetRecord(9, PointClamp, Image(Uint64Texels.data(), Format32_32UInt)); + Keys = {}; + Keys[0] = 9; + const auto added = Run(device, AtomicAdd64LaneTableCode, 1, Lanes, 64); + AgcDriver::Graphics::StorageTexture::FlushAllPending("image table atomic64 test"); + Require(!Faulted(added), "image table atomic64: an add through a table faulted:\n" + added.log); + Require(added.imageTableCount != 0u, "image table atomic64: the add bound no storage table elements"); + for (std::uint32_t x = 0; x < Width; ++x) Require(Uint64Texels[x] == 0x5000000000000000ull + x + x, "image table atomic64: texel " + std::to_string(x) + " is wrong"); + Keys[0] = 6; + const auto null = Run(device, AtomicAdd64LaneTableCode, 1, Lanes, 64); + AgcDriver::Graphics::StorageTexture::FlushAllPending("image table atomic64 test"); + for (std::uint32_t x = 0; x < Width; ++x) Require(Uint64Texels[x] == 0x5000000000000000ull + x + x, "image table atomic64: an add through a null entry reached an image:\n" + null.log); +} + void RunSelectedStoreTests(AgcDriver::VulkanDevice& device) { alignas(256) static std::array storeTexels{}; struct Registration { @@ -1561,6 +1595,7 @@ int main() { RunModeTests(*device); RunStoreTests(*device); RunAtomicTests(*device); + RunAtomic64Tests(*device); RunSelectedStoreTests(*device); RunPointerTests(*device); RunPointerRecordTests(*device); diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp index ae3696883c..ad1a0f01b5 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp @@ -21,6 +21,7 @@ struct ImageTableElement { DescriptorBindingKind kind = DescriptorBindingKind::Buffers; std::uint32_t samplers = 0; bool atomic = false; + bool atomic64 = false; }; struct ImageTableSamplerElement { diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index b3c679dc2e..b9e321b4f0 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -614,6 +614,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con physical.imageDepthCompare.push_back(element.depthCompare); physical.imageWritten.push_back(ImageBindingResourceClass(element.kind) == ImageResourceClass::Storage); physical.imageAtomic.push_back(element.atomic); + physical.imageAtomic64.push_back(element.atomic64); std::uint32_t samplers = 0; for (std::uint32_t sampler = 0; sampler < entry.tableSamplerMasks.size(); ++sampler) { if ((element.samplers & (1u << sampler)) != 0u) samplers |= entry.tableSamplerMasks[sampler]; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index aba7fa1a55..00791c08e9 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -1715,17 +1715,17 @@ ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& i auto element = words.dwords; if (mode.conversionFormat != IrBufferFormat::Invalid || mode.depthBits) element[3] = (element[3] & ~0xfffu) | ShaderImageIdentitySwizzle; const auto kind = DescriptorBindingForImage(mode); - const auto hash = hashWords(element, (static_cast(image.atomic) << 24u) ^ (static_cast(mode.dimension) << 16u) ^ (static_cast(mode.depthCompare) << 8u) ^ static_cast(kind)); + const auto hash = hashWords(element, (static_cast(image.atomic64) << 25u) ^ (static_cast(image.atomic) << 24u) ^ (static_cast(mode.dimension) << 16u) ^ (static_cast(mode.depthCompare) << 8u) ^ static_cast(kind)); auto index = elementIndex.Find(hash, [&](std::uint32_t known) { const auto& entry = out.elements[known]; - return entry.words == element && entry.dimension == mode.dimension && entry.depthCompare == mode.depthCompare && entry.kind == kind && entry.atomic == image.atomic; + return entry.words == element && entry.dimension == mode.dimension && entry.depthCompare == mode.depthCompare && entry.kind == kind && entry.atomic == image.atomic && entry.atomic64 == image.atomic64; }); if (!index.has_value() && out.elements.size() >= Abi::ImageElementBudget) { record[Abi::ImageRecordFlags] = poison(resource, PoisonReason::Budget, &words); } else { if (!index.has_value()) { index = static_cast(out.elements.size()); - out.elements.push_back({element, mode.dimension, mode.depthCompare, kind, 0u, image.atomic}); + out.elements.push_back({element, mode.dimension, mode.depthCompare, kind, 0u, image.atomic, image.atomic64}); elementIndex.Insert(hash, *index); } out.elements[*index].samplers |= directSamplers[resource]; diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 48db089c1c..b8dd43c9cf 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -1707,7 +1707,7 @@ private: continue; } image.table = next++; - if (image.atomic64 || image.packed || image.byElements != 0u) { + if (image.packed || image.byElements != 0u) { image.tableOperation = TableOperation::Unsupported; } } diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index f71cb35955..a22aca2057 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -53,7 +53,7 @@ - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) T# of an MSAA or FMASK image, or of a color format under a depth compare, faults when selected: tables have no multisampled descriptors and no emulated color compare. - Direct [FMASK image loads](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) of 16 samples, CMASK compression (bar the 8-fragment native owner path), packed accesses and other layouts throw. Uncompressed FMASK of up to 8 samples reads a single-mip 2D or 2D-array 8-, 16- or 32-bit `UINT` snapshot with `Z_64KB_X` addressing; selectors may name X, zero or one. `Fmask32_S8_F8` returns the stored word; others expand to 4 bits per sample, an index at or past the fragment count reading 8 (unknown), samples past the count 0. Layouts follow PAL's [expanded values](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9MaskRam.cpp#L5005) and [FMASK surfaces](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L877). Console results, unused nibbles included, are unmeasured; compressed color sampling is unsupported. - A [DIM 2D read of a 2D MSAA T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) returns sample 0, which is not measured on the console's gfx1013, at an implicit LOD above 0, at an explicit one for texels over 16 bits except formats 22 and 56 in SW_64KB_R_X, or for other formats and swizzle modes; 64- and 128-bit SW_64KB_Z_X texels are not covered: gfx1031 does not address their samples. Such a read is an `image_load` without a mip, `image_sample`, `image_sample_lz` or `image_sample_l` of a direct sampled image; other accesses (gathers, bias, gradients, offsets, LOD clamps, depth compares, `image_load_mip`, `image_get_resinfo`, `image_get_lod`, PCK, BY2/BY4) throw, as does a depth-bits T# (16_UINT, 32_UINT or 32_SINT in a Z swizzle mode). On gfx1031 (RX 6700 XT, HIP), such reads return sample 0 at 2x, 4x and 8x for 8- to 128-bit texels in SW_64KB_R_X and 8- to 32-bit ones in SW_64KB_Z_X (formats 1, 5, 7, 11, 13, 14, 18, 22, 29, 56, 60, 64, 71, 77, 133). -- `image_store_mip`, `image_store_pck`, PCK2/PCK4 and MIMG BY2/BY4 through an [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) T# fault (`unsupported operation`) when an active lane executes them; the store is dropped and the load reads zeros. 32-bit image atomics through a table lower to `OpAtomic*` on the table `AtomicView` (`Device` scope, relaxed, `AcquireRelease`/`UniformMemory` barrier, as the direct path) instead of faulting; 64-bit atomics still fault (no `Atomic64View` on the table path). +- `image_store_mip`, `image_store_pck`, PCK2/PCK4 and MIMG BY2/BY4 through an [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) T# fault (`unsupported operation`) when an active lane executes them; the store is dropped and the load reads zeros. 32-bit and 64-bit image atomics through a table lower to `OpAtomic*` on the table `AtomicView`/`Atomic64View` (`Device` scope, relaxed, `AcquireRelease`/`UniformMemory` barrier as the direct path) instead of faulting; the host needs `VK_FORMAT_FEATURE_STORAGE_IMAGE_ATOMIC_BIT` for the view format. Packed formats and BY element counts still fault. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) S# with a min or max reduction or FORCE_UNNORMALIZED faults when selected, as does an integer, converted or depth-bits table T# paired with a direct min or max S# that filters between texels. - Uint and Sint [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp) entries and direct T#s under a table S# sample through a point-filtered copy of the S#: Vulkan leaves linear filtering of integer formats undefined; the console is assumed to point-sample. - Comparison sampling of a color texture through an [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) S# throws at the first dispatch or draw that binds it. From 0c5e45e9f361ed3ad6cd641f4f1c8a57162f0b8e Mon Sep 17 00:00:00 2001 From: dantdmdarealguy Date: Wed, 21 Oct 2026 18:08:55 -0500 Subject: [PATCH 168/428] docs: describe the converted module output the relinker prints The relinker stopped printing a 'Guest module:' line for each converted module in 56c2a5318; it reports 'Converted modules: N' and lists the .guest.prx files under app0/ in its runtime layout tree. GAME_TESTING.md and USAGE.md still told readers to use the files printed as 'Guest module:'. --- docs/dev/GAME_TESTING.md | 2 +- docs/user/USAGE.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/GAME_TESTING.md b/docs/dev/GAME_TESTING.md index 6e74085f0e..82afb1ccfc 100644 --- a/docs/dev/GAME_TESTING.md +++ b/docs/dev/GAME_TESTING.md @@ -12,7 +12,7 @@ For user options see [USAGE](../user/USAGE.md); for internals see [ARCHITECTURE] - Linux: `relinker --registry src/eboot.elf app.elf` - Windows: `relinker --windows --registry src/eboot.elf app.exe`. Without `--windows` the output is an ELF file whatever its name, and Windows refuses to start it (exit code 216, "not compatible with the version of Windows"). - A dump whose `app0` holds `ampr_assets.index` and `ampr_assets-*.pak` was repacked for [ampr_emu](https://github.com/drakmor/ampr_emu): the packed files were removed from `app0`, and only ampr_emu reads the packs. Restore them first with ampr_emu's `tools/ampr_pack.py unpack --index app0/ampr_assets.index --output ` and copy the files missing from `app0`. Without them a Unity title stops at start-up on the missing `Media/Resources/unity default resources`. -- Convert every module the title loads from its own folders (`sce_module/`, `Media/Modules/`, `Media/Plugins/`). Unity titles need `--module-dir Media/Modules --module-dir Media/Plugins`. Put each printed `Guest module:` file next to its original `.prx` in `app0/`, and keep the original there. +- Convert every module the title loads from its own folders (`sce_module/`, `Media/Modules/`, `Media/Plugins/`). Unity titles need `--module-dir Media/Modules --module-dir Media/Plugins`. The relinker writes each converted module as `.guest.prx` into `app0/` beside the output, in the module's own folder, and lists it in the `Arrange the files like this` tree it prints. Keep the original `.prx` next to it. - `libs/` holds only AnyPS5's own libraries, from `build/core/libs/libs` (not `unpatched/`). A game module placed there fails to load (error 193 on Windows). A self-built `libs/` on Windows also needs `libgcc_s_seh-1.dll`, `libstdc++-6.dll` and `libwinpthread-1.dll` from the toolchain's `mingw64/bin`, or every library fails with error 126 ([USAGE](../user/USAGE.md#input-and-conversion)). - Run `tools/import_audit.py` on the registries. Exit code 1 lists imports that will fail or throw. - Run one title at a time. Titles share the GPU and the controller file. Some titles start a second process (Peace Walker's `mgspw.mgspw.elf`) that can outlive a time cap on the first: check that every process of the previous run is gone. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 3a08d85d7c..8847946157 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -174,7 +174,7 @@ Some applications also need their original `app0/sce_sys/param.json` at runtime. Resources can also be packed into `app0.zar` beside `app0/`, a [ZArchive](https://github.com/Exzap/ZArchive) whose root is the `app0/` directory. A file present in `app0/` is read from there and any other file from the archive; the executable, `sce_module/` and `sce_sys/` stay in `app0/`. The archive is read only through `sceKernelOpen`/`open`, the read, seek and stat calls on its descriptors and `sceKernelStat`; other calls on an archived path, listing a directory that the archive also contains and opening a file in it for writing fail as described in [technical debt](../dev/debt/libkernel.md). An archive that cannot be opened stops the title at its first file access. -Use the generated files printed as `Guest module:` for bundled title modules. `libs/` is for AnyPS5 system libraries, not the original PS5 `.prx` files. Placing an original PS5 module in `libs/` on Windows makes Windows try to load it as a DLL and can fail with error 193 (not a valid Win32 application). A module the title loads while it runs that was not relinked, such as one in a directory not given to `--module-dir`, is not loaded either: the title gets an error and continues, and the runtime prints `[module] dlopen: was not relinked: ...` once for each file. Relink that module's directory with `--module-dir `. +For bundled title modules, use the `.guest.prx` files the relinker writes into `app0/` and lists in the layout it prints. `libs/` is for AnyPS5 system libraries, not the original PS5 `.prx` files. Placing an original PS5 module in `libs/` on Windows makes Windows try to load it as a DLL and can fail with error 193 (not a valid Win32 application). A module the title loads while it runs that was not relinked, such as one in a directory not given to `--module-dir`, is not loaded either: the title gets an error and continues, and the runtime prints `[module] dlopen: was not relinked: ...` once for each file. Relink that module's directory with `--module-dir `. On Windows, a self-built `libs/` also needs `libgcc_s_seh-1.dll`, `libstdc++-6.dll` and `libwinpthread-1.dll` from the `mingw64/bin` directory of the toolchain that built the libraries; the release archives already contain them. The libraries are loaded without searching `PATH`, so a copy elsewhere on the system is not used, and a library that needs one of them fails with error 126 (the specified module could not be found) although its `.prx` file is present. From 8b83c68aad9ac9444c6f6dc3b82c753b34556ab6 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 22 Oct 2026 13:25:28 -0400 Subject: [PATCH 169/428] fix(libc): compute exp2f with FreeBSD msun's algorithm, as titles' libc.prx does exp2f returned the host exp2, so its last bit and errno depended on the host: on Windows the bits differ from the libc.prx that titles ship on 213,306,266 of the 2^32 floats. That libc.prx is msun's s_exp2f.c, setting ERANGE only in its overflow and underflow branches. The port gives the same bits and errno as PPSA07923's, PPSA05916's and PPSA13934's copies for every argument. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Math.cpp | 44 ++++++++++++++++++++++++++++++++- core/libs/tests/GuestMath.cpp | 22 +++++++++++++++++ docs/dev/debt/libc.md | 1 + 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 55ffd82dd0..30b21c1225 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -328,6 +328,49 @@ double APS5_VABI hypot_nid_postfix(double x, double y) { float APS5_VABI tanf_nid_postfix(float x) { return std::tan(x); } float APS5_VABI log10f_nid_postfix(float x) { return std::log10(x); } float APS5_VABI logbf_nid_postfix(float x) { return std::logb(x); } +float APS5_VABI exp2f_nid_postfix(float x) { + static constexpr double exp2ft[16] = { + 0x1.6a09e667f3bcdp-1, 0x1.7a11473eb0187p-1, 0x1.8ace5422aa0dbp-1, 0x1.9c49182a3f090p-1, + 0x1.ae89f995ad3adp-1, 0x1.c199bdd85529cp-1, 0x1.d5818dcfba487p-1, 0x1.ea4afa2a490dap-1, + 0x1.0000000000000p+0, 0x1.0b5586cf9890fp+0, 0x1.172b83c7d517bp+0, 0x1.2387a6e756238p+0, + 0x1.306fe0a31b715p+0, 0x1.3dea64c123422p+0, 0x1.4bfdad5362a27p+0, 0x1.5ab07dd485429p+0, + }; + static volatile float huge = 0x1p100f; + static volatile float twom100 = 0x1p-100f; + const float redux = 0x1.8p23f / 16; + const float P1 = 0x1.62e430p-1f; + const float P2 = 0x1.ebfbe0p-3f; + const float P3 = 0x1.c6b348p-5f; + const float P4 = 0x1.3b2c9cp-7f; + const std::uint32_t hx = std::bit_cast(x); + const std::uint32_t ix = hx & 0x7fffffff; + if (ix >= 0x43000000) { + if (ix >= 0x7f800000) { + return (ix & 0x7fffff) != 0 || (hx & 0x80000000) == 0 ? x + x : 0.0f; + } + if (x >= 0x1.0p7f) { + errno = ERANGE; + return huge * huge; + } + if (x <= -0x1.2cp7f) { + errno = ERANGE; + return twom100 * twom100; + } + } else if (ix <= 0x33000000) { + return 1.0f + x; + } + volatile float rounded = x + redux; + float t = rounded; + std::uint32_t i0 = std::bit_cast(t) + 8; + const std::int32_t k = static_cast(i0 >> 4) << 20; + i0 &= 15; + t -= redux; + const double z = x - t; + const double twopk = std::bit_cast(static_cast(static_cast(0x3ff00000 + k)) << 32); + const double u = exp2ft[i0] * z; + const double tv = exp2ft[i0] + u * (P1 + z * P2) + u * (z * z) * (P3 + z * P4); + return static_cast(tv * twopk); +} double APS5_VABI exp2_nid_postfix(double x) { int saved_errno = errno; double result = std::exp2(x); @@ -640,7 +683,6 @@ double APS5_VABI __powidf2_nid_postfix(double base, int exponent) { return reciprocal ? 1.0 / result : result; } float APS5_VABI expf_nid_postfix(float x) { return std::exp(x); } -float APS5_VABI exp2f_nid_postfix(float x) { return std::exp2(x); } float APS5_VABI logf_nid_postfix(float x) { return std::log(x); } float APS5_VABI log2f_nid_postfix(float x) { return std::log2(x); } double APS5_VABI log10_nid_postfix(double x) { return std::log10(x); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 4014e36484..4358b07534 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -140,6 +140,7 @@ float APS5_VABI asinhf_nid_postfix(float); double APS5_VABI acosh_nid_postfix(double); float APS5_VABI acoshf_nid_postfix(float); double APS5_VABI atanh_nid_postfix(double); +float APS5_VABI exp2f_nid_postfix(float); float APS5_VABI atanhf_nid_postfix(float); std::int64_t APS5_VABI lroundf_nid_postfix(float); std::int64_t APS5_VABI llround_nid_postfix(double); @@ -930,6 +931,26 @@ static void CheckExtendedMath() { Require(std::isnan(remainder_nid_postfix(1.0, 0.0)) && std::isnan(remainder_nid_postfix(INFINITY, 1.0))); } +static void CheckExp2fConsoleBits() { + constexpr struct { + std::uint32_t input; + std::uint32_t expected; + int error; + } cases[] = { + {0x3d00010bu, 0x3f82cd8du, 0}, {0x3f000303u, 0x3fb5066du, 0}, {0x40a0051bu, 0x42000e29u, 0}, {0x42f0070cu, 0x7b813a1bu, 0}, + {0xbf400909u, 0x3f183437u, 0}, {0xc2f00b14u, 0x037c3073u, 0}, {0x00000000u, 0x3f800000u, 0}, {0x80000000u, 0x3f800000u, 0}, + {0x33000000u, 0x3f800000u, 0}, {0x3f800000u, 0x40000000u, 0}, {0x42fe0000u, 0x7f000000u, 0}, {0x42ffffffu, 0x7f7fffa7u, 0}, + {0x43000000u, 0x7f800000u, ERANGE}, {0x7f7fffffu, 0x7f800000u, ERANGE}, {0x7f800000u, 0x7f800000u, 0}, {0xff800000u, 0x00000000u, 0}, + {0xc2fc0000u, 0x00800000u, 0}, {0xc3150000u, 0x00000001u, 0}, {0xc315ffffu, 0x00000001u, 0}, {0xc3160000u, 0x00000000u, ERANGE}, + {0xc3160001u, 0x00000000u, ERANGE}, {0xff7fffffu, 0x00000000u, ERANGE}, {0x7fc00001u, 0x7fc00001u, 0}, {0xffc00002u, 0xffc00002u, 0}, + }; + for (const auto& item : cases) { + errno = 1234; + Require(std::bit_cast(exp2f_nid_postfix(std::bit_cast(item.input))) == item.expected); + Require(errno == (item.error != 0 ? item.error : 1234)); + } +} + static void CheckHyperbolic() { const double infinity = std::numeric_limits::infinity(); Require(_Cosh_nid_postfix(0.0, 2.0) == 2.0); @@ -1724,6 +1745,7 @@ int main() { Require(__powidf2_nid_postfix(2.0, -127) == 0x1p-127); CheckHyperbolic(); CheckCoshfMsunBoundaries(); + CheckExp2fConsoleBits(); CheckScaledHyperbolic(); CheckSinhf(); CheckAtanhf(); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index a7e892c6a0..f11430b924 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -143,6 +143,7 @@ - [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) follows Dinkumware `thread0.cpp` ([VC12](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/thread0.cpp), [STL](https://github.com/microsoft/STL/blob/da52dc0f/stl/src/thread0.cpp)), without console verification. Codes outside 0..6 throw `std::out_of_range`, the deleting destructor throws, and STREAMS errnos absent from FreeBSD 11.4 [`errno.h`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h) (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are omitted. Codes 0..6 throw a 32-byte [VC12 `system_error`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/system_error) with FreeBSD errnos (16, 22, 3, 12, 1, 11, 35), a static `generic_category` (not `_Generic_object`), and unverified 40-byte [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) messages from [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/syserror.cpp) - [Call tracing](../../../core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) is missing on macOS and records only the entry of a call, not its arguments or result. One process-wide ring keeps the last 65536 calls, so a busy thread pushes the others out; the reporting thread's own calls are listed only while they are still in that ring. Calls are counted per traced target; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; wider vector arguments survive only while the recording code avoids AVX, which is untested. Linux has no crash report, so it prints the trace only on `SIGABRT` (guest or host `abort()`), not on `SIGSEGV`; a caller in a module loaded after the last traced import prints without a module name, and a thread aborting while another writes the report waits for it at most 2 seconds - [_Dtest](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows Dinkumware double classification matching _FDtest: finite values return -1, subnormals -2, zeros 0, infinities 1, and NaNs 2. +- [exp2f](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`s_exp2f.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_exp2f.c), with `huge` and `twom100` read at run time and `x + redux` rounded to float, as msun's `volatile` reads and `STRICT_ASSIGN` do. As in the `libc.prx` that titles ship, `errno` is set to `ERANGE` only in msun's overflow (`x >= 128`) and underflow (`x <= -150`) branches, so a subnormal result leaves it unchanged. Floating-point exceptions are not compared. It gives the same bits and `errno` as the `exp2f` of the `libc.prx` bundled with PPSA07923, PPSA05916 and PPSA13934 for all 2^32 arguments, run on a Windows host. - [app0.zar](../../../core/libs/prx/libc/src/AppArchive.cpp) (libc) - a [ZArchive](https://github.com/Exzap/ZArchive) beside `app0/` is a read-only layer under it: a path missing from `app0/` is looked up in the archive, case-insensitively. It is not a console format; titles ship their files in a PFS image instead. Resolving an archive-only path through any call other than the archive-aware libkernel ones throws, so `fopen`, `stat`, `access`, `opendir` and the rest do not read the archive yet, and `opendir` of a directory that exists in both places, `app0/` itself included, and `fdopen` of an archived descriptor throw instead of missing its entries or reading the null device. A damaged compressed block throws instead of returning `EIO` - [fopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - returns the errno of the failed open, as C11 K.3.5.2.1 leaves the nonzero value open and Microsoft's CRT returns errno; the console's Dinkumware `fopen_s` is not verified - [ios_base standard streams](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) `_Addstd` queues the stream in the standard stream table of Dinkumware [ios.cpp](https://github.com/ganboing/crt.vc12/blob/master/src/ios.cpp) (`stdstr`, `stdopens`, `_Addstd`, `_Ios_base_dtor`): it takes the first of slots 1 to 7 that is free or already holds the stream, and slot 8 (`NSTDSTR`) when none is, so slot 8 is shared by every stream past the first seven and registration never fails. It stores the 1-based slot index in the guest `IosBase::standardStream` (Dinkumware's `_Stdstr`, where 0 means not a standard stream) and counts the stream objects registered per slot; registering the same stream again reuses its slot and raises its count. Measured on a PS5 (firmware 13.60) from a homebrew payload calling the system `libSceLibcInternal` export: in a process whose own standard streams already hold slots 1 to 7, twelve distinct streams and a repeated one all get 8, written as a 64-bit value at offset 0x8 with no other byte of the object touched; the assignment of slots 1 to 7 to fresh streams is not observable there and follows VC12. The counts are `int`, not VC12's `char`, and the registration and the destructor lock the table where VC12 locks only `_Addstd`; a slot index past the table throws in the destructor instead of indexing out of bounds. The destructor decrements its slot's count and suppresses the rest of the destruction while the count stays positive, as `_Ios_base_dtor` does, and the slots of destroyed streams keep their pointer and stay reserved for that stream, as `stdstr` does. The streams themselves are destroyed by the title's own code as it registers them through `__cxa_atexit`; when the last reference closes, the unsupported stream storage and callback cleanup is still the throw it was and the locale is released as for a non-standard stream. From ce6865fc0dbfb8ee4aa689137d2a1000d868c79a Mon Sep 17 00:00:00 2001 From: Ethan Barr Date: Thu, 22 Oct 2026 16:00:16 -0400 Subject: [PATCH 170/428] perf(agc): pipeline queue-0 DMA_DATA by default Queue-0 DMA_DATA packets drained the draw pipeline unless APS5_PIPELINE_DMA was set, so the packet worker waited on the committer at every small immediate store. Kena PPSA01746 issues 17-23k DMA_DATA packets per 10 s and 11329 of its 20134 pipeline drains per 10 s are opcode 0x50. enqueueDmaPacket now queues DMA_DATA stores and fills unconditionally, and keepsDrawPipeline keeps the pipeline for a copy or fill whose source and destination miss every queued write. The APS5_PIPELINE_DMA switch and its documentation are removed and the tests run the default path. --- .../Execution/src/Driver/Packets/PacketExecution.cpp | 6 ------ .../Execution/src/Driver/Packets/PacketMemory.cpp | 3 +-- core/libs/prx/libSceAgcDriver/tests/Submit.cpp | 3 +-- .../libSceAgcDriver/tests/execution/PipelinedDispatch.cpp | 2 -- .../prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp | 2 -- .../tests/execution/PipelinedMetadataPass.cpp | 2 -- .../tests/execution/QueuedIndexReadOrder.cpp | 2 -- docs/dev/ARCHITECTURE.md | 2 +- docs/dev/ENVIRONMENT.md | 1 - 9 files changed, 3 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index 08b69be4d4..f0850f0dfc 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -29,11 +29,6 @@ bool orderedFills() { return ordered; } -bool pipelineDma() { - static const bool enabled = std::getenv("APS5_PIPELINE_DMA") != nullptr; - return enabled; -} - bool keepsDrawFinisher(std::uint32_t header, std::uint32_t opcode) { if (header == FlipPacketHeader || header == RenderingWaitPacketHeader) return false; if (Pm4::DrawOpcode(opcode)) return true; @@ -65,7 +60,6 @@ bool keepsDrawPipeline(std::span packet, std::uint32_t head if (packet.size() < 7) return false; const auto destination = Pm4::DmaDestination(packet); if (destination == Pm4::DmaDestinationNowhere) return true; - if (!pipelineDma()) return false; if (destination == Pm4::DmaSelectGds) return true; const auto bytes = Pm4::DmaBytes(packet); if (bytes == 0) return true; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp index 5c2cc06454..7f11880db5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -292,8 +292,7 @@ void Driver::dumpSampleCounters(std::uint64_t address) { } bool Driver::enqueueDmaPacket(std::span packet, std::uint32_t opcode, std::uint32_t queue, const QueueState& state) { - static const bool enabled = std::getenv("APS5_PIPELINE_DMA") != nullptr; - if (!enabled || opcode != 0x50 || queue != 0 || packet.size() < 7 || !DrawPipeline::Active() || !deferredLabels().labels.empty()) return false; + if (opcode != 0x50 || queue != 0 || packet.size() < 7 || !DrawPipeline::Active() || !deferredLabels().labels.empty()) return false; const auto source = ((packet[1] >> 29u) & 3u) | ((packet[6] >> 24u) & 4u) | ((packet[6] >> 25u) & 8u); constexpr std::size_t immediateLimit = std::size_t{16} << 20u; constexpr std::size_t gpuStoreLimit = 65536; diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index d97862941b..ae7e29ee60 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -593,8 +593,7 @@ void testSmallCopyBehindPendingSource() { waitFor(&copied, 1, "the label after a small copy behind a pending source never landed"); AgcDriverWaitIdle_nid_postfix(); check(std::equal(destination, destination + smallWords, produced), "a small copy did not see the GPU copy into its source before it"); - if (std::getenv("APS5_COPY_SYNC") != nullptr) check(AgcDriver::DriverDetail::copiesBehindPendingSources.load() == before, "a small copy that has to wait for a completion store over its source went through the GPU copy"); - else check(AgcDriver::DriverDetail::copiesBehindPendingSources.load() == before + 1, "a small copy whose source the GPU still stores into was not recorded on the GPU"); + check(AgcDriver::DriverDetail::copiesBehindPendingSources.load() == before, "a small copy queued behind the pipeline write into its source was recorded inline"); std::fill(destination, destination + smallWords, 0u); const auto settled = AgcDriver::DriverDetail::copiesBehindPendingSources.load(); submit(0, commands(memoryCopy(destination, produced, smallBytes), writeData(&settledCopied, 1))); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDispatch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDispatch.cpp index 8cc143e3c5..4996854346 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDispatch.cpp @@ -132,10 +132,8 @@ void Submit(std::vector& words) { int main() { #ifdef _WIN32 _putenv_s("APS5_PIPELINED_DRAWS", "1"); - _putenv_s("APS5_PIPELINE_DMA", "1"); #else setenv("APS5_PIPELINED_DRAWS", "1", 1); - setenv("APS5_PIPELINE_DMA", "1", 1); #endif try { { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp index d43e135c35..7a62e1e7cf 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp @@ -74,10 +74,8 @@ void ExpectNoDrain(std::uint32_t opcode, std::uint64_t before, const char* what) int main() { #ifdef _WIN32 _putenv_s("APS5_PIPELINED_DRAWS", "1"); - _putenv_s("APS5_PIPELINE_DMA", "1"); #else setenv("APS5_PIPELINED_DRAWS", "1", 1); - setenv("APS5_PIPELINE_DMA", "1", 1); #endif try { alignas(256) static std::array first{}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedMetadataPass.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedMetadataPass.cpp index 7fa9eb1f07..0410e4c9a6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedMetadataPass.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedMetadataPass.cpp @@ -120,11 +120,9 @@ void expectEliminated(const std::string& what) { int main() { #ifdef _WIN32 _putenv_s("APS5_PIPELINED_DRAWS", "1"); - _putenv_s("APS5_PIPELINE_DMA", "1"); _putenv_s("APS5_DRAW_PIPELINE", "1"); #else setenv("APS5_PIPELINED_DRAWS", "1", 1); - setenv("APS5_PIPELINE_DMA", "1", 1); setenv("APS5_DRAW_PIPELINE", "1", 1); #endif try { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/QueuedIndexReadOrder.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/QueuedIndexReadOrder.cpp index acc2b1617a..63a2aaeadc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/QueuedIndexReadOrder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/QueuedIndexReadOrder.cpp @@ -343,10 +343,8 @@ void CopyPendingImage(std::size_t bytes, const char* phase) { int main() { #ifdef _WIN32 _putenv_s("APS5_PIPELINED_DRAWS", "1"); - _putenv_s("APS5_PIPELINE_DMA", "1"); #else setenv("APS5_PIPELINED_DRAWS", "1", 1); - setenv("APS5_PIPELINE_DMA", "1", 1); #endif try { { diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 387d084365..6c5e58c6ff 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -128,7 +128,7 @@ flowchart LR - A compute shader whose LDS plus its lock dword exceeds the host's `maxComputeSharedMemorySize` is not given a `Workgroup` array. `Recompile` sets `ShaderInfo::sharedMemoryBytes` for it and the SPIR-V backend addresses the LDS in a coherent storage buffer at descriptor set 1 (`WorkgroupMemoryDescriptorSet`), with a slice of `RecompileResult::workgroupMemoryDwords` per workgroup, indexed by `WorkgroupID` and `NumWorkGroups`. `VulkanDevice` keeps one such buffer, sized from the dispatch dimensions, and binds it after the resource set; programs that fit keep using shared memory. - `Driver::RegisterShader` splits shader preparation in two. `PlanRegistered` runs on the registering guest thread: it validates the registers and entry point, decodes the code and builds the recompile request, so invalid registrations still fail there. The RDNA to IR translation and resource plan (`PrepareShader`) run on a background pool of `hardware_concurrency() / 2` detached threads (at least 2, 64 MiB stacks). - A thread that needs the GPU mutex, the committer's next item or the end of the committer's queue spins before it blocks (`HandoffSpin.hpp`): `GpuMutexType::lock` retries `try_lock` for the window, the committer polls its item count, and `DrawPipeline::Drain` polls it for twice the window, so a hand-off that completes within the window costs no kernel wake-up. The window is 150 us when the process may run on more than 8 logical processors (its affinity mask, via `CpuTopology`) and 0 otherwise, where the spinning threads would compete with the holder; `APS5_HANDOFF_SPIN_US=` sets it (0 turns it off, at most 10000). Ordering and the lock's semantics are unchanged. -- Queue 0's packet worker decodes, captures and prepares a draw and hands its recording (`Graphics::Draw` under the GPU lock) to a committer thread in [`DrawPipeline.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp), so the next packet is decoded while the previous draw is recorded. Each queued item carries the guest ranges it will write (`DrawWriteRanges`: written buffers and storage images, color targets with their DCC and CMASK, depth and stencil). A later packet that reads or writes one of those ranges, or that the pipeline can't order, drains it first; labels, submission ends, buffer fills and (with `APS5_PIPELINE_DMA=1`) DMA packets are queued as items instead. With `APS5_PIPELINE_DISPATCH=1` (off by default), a `DISPATCH_DIRECT` whose dispatch-cache entry hits with a recipe is queued as an item too, carrying the dispatch's written buffers and storage images (`DispatchWriteRanges`); the capture and the cache lookup stay on the worker and the recipe is recorded on the committer. A dispatch still drains when its lookup misses, when what it reads overlaps a write still pending, when the hit is verified against data or when its writes cannot be ranged (bindless access, GDS, fault buffer). An indirect draw resolved on the CPU (its records read by the worker) drains only when its records or its count overlap a queued write; the records it draws inline drain first, and its last drawn record is queued like a direct draw (`APS5_NO_PIPELINED_INDIRECT=1` drains at every indirect draw). An exception thrown by a commit is rethrown on the worker at its next enqueue or drain, as an inline draw error would be. On by default; `APS5_PIPELINED_DRAWS=0` records every draw inline, and `APS5_EXACT_DRAW_WRITES=0` widens the color and depth ranges to estimates. +- Queue 0's packet worker decodes, captures and prepares a draw and hands its recording (`Graphics::Draw` under the GPU lock) to a committer thread in [`DrawPipeline.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp), so the next packet is decoded while the previous draw is recorded. Each queued item carries the guest ranges it will write (`DrawWriteRanges`: written buffers and storage images, color targets with their DCC and CMASK, depth and stencil). A later packet that reads or writes one of those ranges, or that the pipeline can't order, drains it first; labels, submission ends, buffer fills and DMA packets are queued as items instead. With `APS5_PIPELINE_DISPATCH=1` (off by default), a `DISPATCH_DIRECT` whose dispatch-cache entry hits with a recipe is queued as an item too, carrying the dispatch's written buffers and storage images (`DispatchWriteRanges`); the capture and the cache lookup stay on the worker and the recipe is recorded on the committer. A dispatch still drains when its lookup misses, when what it reads overlaps a write still pending, when the hit is verified against data or when its writes cannot be ranged (bindless access, GDS, fault buffer). An indirect draw resolved on the CPU (its records read by the worker) drains only when its records or its count overlap a queued write; the records it draws inline drain first, and its last drawn record is queued like a direct draw (`APS5_NO_PIPELINED_INDIRECT=1` drains at every indirect draw). An exception thrown by a commit is rethrown on the worker at its next enqueue or drain, as an inline draw error would be. On by default; `APS5_PIPELINED_DRAWS=0` records every draw inline, and `APS5_EXACT_DRAW_WRITES=0` widens the color and depth ranges to estimates. - A dispatch's resource build runs stage A (binding plan, buffers, descriptor set) without the GPU lock and stage B (image lookups) under it. Between them `VulkanDevice::presync` waits, still without the lock, for recorded work that writes a surface stage B would read on the CPU and for recorded work that writes the DCC keys of an image the dispatch binds (`ShaderResources::PresyncSurfaces`), so the locked `WaitForKeyWriters` finds nothing pending to synchronize with. A key range is waited for only when recorded work writes it, the key-store memo does not give its value (`DccKeyWritersPending`, without the mutex) and the pending stores do not give it either (`DccKeyWriterNeedsSync`, under the mutex, the same test `WaitForKeyWriters` makes before it synchronizes). Stage A's own key scan runs before that wait, and stage B compares it with the keys it reads under the lock as before. `APS5_NO_DCC_PRESYNC=1` leaves the key ranges out; `APS5_NO_PRESYNC=1` disables the whole pre-sync. - `APS5_DRAW_PIPELINE=1` (off by default, meant with `APS5_PIPELINED_DRAWS=32 APS5_PIPELINE_DISPATCH=1`) takes more of a draw off the worker. A `DrawFinisher` thread runs the second half of a direct pipelined draw (stage materialization, the draw cache insert and the hand-off to the committer) while the worker reads the next packets; the worker drains it before the next draw's cache lookup, before packets other than register writes and draws, and before guest reads, and finishes in place a draw whose stages may write memory (`ProgramWritesMemory`) or write through the BDA page table. The committer skips the fragment-output validation and storage-image layer scans whose inputs match its last commit (`CommitTrims`), and a resource or storage image proved current keeps its proof for the rest of the collect epoch until `GuestMemory::BreakProofs` (a CPU write, a write-back, a recorded write) moves `ProofSerial`. Color metadata passes go through the pipeline with their write ranges (`PipelinedColorMetadataPassWrites`) instead of draining it. A data-only hit whose shape entry belongs to an older draw replaces that entry in place at the second sight of the same read user words. On Linux the main image's pages get a page table entry, and capture pages are pooled across threads. `APS5_VERIFY_DRAW_PIPELINE=1` runs a full check next to 1 in 64 reused proofs and prints `[epoch-proofs]` counts. - The shader registry maps a code address to every header registered for that code, in registration order. Static, graphics and rectangle ABI resolution pick the header at the bound address (or one equal to it, `SameHeader`, which also takes a hull header fused by `sceAgcFuseShaderHalves` (type 3) as the registered back half (type 7) it copies, the fused registers aside; graphics ABI resolution then uses the fused header's shader registers); draws and dispatches pick the program by the stage's binary type (`RegisteredProgram`), and a front entry matching both type 2 and type 4 throws. Within a type the last registration wins; re-registering an identical earlier header makes its snapshot current again without preparing it again, and changed code drops the older headers. diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 27fc474b9c..100b3b7179 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -337,7 +337,6 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_PACKET_EPOCH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitStatistics.cpp` | | makes queue workers bump the collect epoch before every packet | | `APS5_PIPELINED_DRAWS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp` | 8 | depth of the queue-0 draw pipeline: 0 records draws inline, 1 selects the default depth of 8, and any other value is the depth capped at 256 | | `APS5_PIPELINE_DISPATCH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | records an eligible dispatch as an item in the queue-0 pipeline instead of running it inline | -| `APS5_PIPELINE_DMA` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | records a queue-0 DMA data packet in the draw pipeline instead of executing it inline | | `APS5_PIPELINE_DRAIN_FILLS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp` | | runs queue-0 buffer fills through the draw pipeline to keep their order; any value stores them in place instead | | `APS5_PIPELINE_DRAIN_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | answers a queue-0 wait from the draw pipeline's own pending labels while it is busy; any value drains the pipeline at the wait | | `APS5_PIPELINE_DRAIN_SUBMISSIONS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | lets a submission end without submitting its recorded work, so it joins a later submit with the pipeline's queue; any value drains the pipeline and submits at the end of the submission | From bdbf14f7120b0c8551977417f01a0f020722167b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 24 Oct 2026 01:53:49 +0200 Subject: [PATCH 171/428] test(agc): expect a non-overlapping DMA_DATA copy to stay on the draw pipeline Co-Authored-By: Claude Sonnet 5.5 --- .../libSceAgcDriver/tests/execution/PipelinedPrefetch.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedPrefetch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedPrefetch.cpp index cf3574966c..fe2b116f63 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedPrefetch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedPrefetch.cpp @@ -106,12 +106,12 @@ int main() { const auto prefetched = SubmitBehindBusyPipeline(prefetch, 0x11111111u, true); check(prefetched.outstanding >= 2, "a DMA_DATA prefetch drained the draw pipeline: the label behind it never queued behind the held commit (" + std::to_string(prefetched.outstanding) + " outstanding)"); check(prefetched.drains == 0, "a DMA_DATA prefetch drained the draw pipeline (" + std::to_string(prefetched.drains) + " packet drains)"); - const auto copied = SubmitBehindBusyPipeline(copy, 0x22222222u, false); - check(copied.outstanding == 1, "a DMA_DATA copy did not hold the queue worker (" + std::to_string(copied.outstanding) + " outstanding)"); - check(copied.drains == 1, "a DMA_DATA copy did not drain the draw pipeline (" + std::to_string(copied.drains) + " packet drains)"); + const auto copied = SubmitBehindBusyPipeline(copy, 0x22222222u, true); + check(copied.outstanding >= 2, "a DMA_DATA copy that misses every queued write drained the draw pipeline: the label behind it never queued behind the held commit (" + std::to_string(copied.outstanding) + " outstanding)"); + check(copied.drains == 0, "a DMA_DATA copy that misses every queued write drained the draw pipeline (" + std::to_string(copied.drains) + " packet drains)"); for (std::size_t i = 0; i < Destination.size(); ++i) check(Destination[i] == (i < 128 ? Source[i] : 0u), "the copy did not land, or wrote past its bytes"); LibcRunShutdown_nid_postfix(); - std::puts("a DMA_DATA prefetch keeps the draw pipeline, a copy drains it"); + std::puts("a DMA_DATA prefetch and a copy that misses every queued write keep the draw pipeline"); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From 17c723d8fa9343bcc7a5bf88265996cf2ebe7423 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Thu, 22 Oct 2026 02:20:41 +0700 Subject: [PATCH 172/428] docs(libc): correct stale rmdir, nanf and bad_optional_access debt statements Co-Authored-By: Claude --- docs/dev/debt/libc.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index a7e892c6a0..f9fc90c3b3 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -89,8 +89,8 @@ - [_Stod](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libSceLibcInternal, implemented in libc) throws for hexadecimal text with a non-zero power-of-ten scale. The console's result there does not follow a simple rule: `0x3` with a scale of -1 gives 0.29999999999999999 - [Long double math in MathExact.cpp](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) was compared with the `libc.prx` bundled with PPSA08427 only for canonical 80-bit encodings; unnormals, pseudo-denormals and pseudo-infinities passed to arithmetic functions (not the classifiers) follow the host x87 instead of the console - [atanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`s_atanf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_atanf.c), with `atanlo[3]` read at run time as msun's `volatile` does. Tiny arguments return `x` without raising `FE_INEXACT`. It gives the same bits as the `atanf` of the `libc.prx` bundled with PPSA07923, PPSA05916 and PPSA13934 for all 2^32 arguments, run on a Windows host, and none of those sets `errno`. Whether other titles' builds differ is unverified. -- [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `rmdir("dir/.")`, `unlink("dir/.")` and the same calls with `..` follow the same lookup rule in FreeBSD but are not changed here: the guest still resolves the dot component away and acts on `dir` or its parent. Both arguments are checked before any host access, so `rename` of a missing source onto `dir/.` answers `EINVAL`, where FreeBSD's [`kern_renameat`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_syscalls.c) looks the source up first and answers `ENOENT`. A backslash counts as a path separator here (the guest path resolver turns it into `/`); FreeBSD treats it as an ordinary name character. None of this was measured on a console. -- [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. +- [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `unlink`, `rmdir`, `sceKernelUnlink` and `sceKernelRmdir` follow the same rule. Both arguments are checked before any host access, so `rename` of a missing source onto `dir/.` answers `EINVAL`, where FreeBSD's [`kern_renameat`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_syscalls.c) looks the source up first and answers `ENOENT`. A backslash counts as a path separator here (the guest path resolver turns it into `/`); FreeBSD treats it as an ordinary name character. None of this was measured on a console. +- [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) imports nanf. - [jrand48 and mrand48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) return the high 32 bits of the new 48-bit state zero-extended to 64 bits, as the console's `libSceLibcInternal.sprx` does when called from a homebrew payload process on a PS5 (firmware 12.70): a result with bit 31 set comes back between 0x80000000 and 0xFFFFFFFF, as FreeBSD 11.0's [`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/jrand48.c#L23) and [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/mrand48.c#L25) return it; FreeBSD 11.1 and later cast it to `int32_t` ([`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/jrand48.c#L26), [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/mrand48.c#L28)), which makes it negative. `lrand48` and `nrand48` matched FreeBSD there. Whether a title's own libc does the same is unverified - [seed48 and lcong48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x6F460 and 0x6F420): the multiplier and addend are shared by the whole `rand48` family, including `erand48`, `nrand48` and `jrand48` on a caller's seed; `lcong48` sets them with the seed, and `srand48` and `seed48` restore 0x5DEECE66D and 0xB. Unlike FreeBSD, `seed48` returns a static copy of the new seed rather than the previous one. A null argument, which the console dereferences, throws - [lgamma and signgam](../../../core/libs/prx/libc/src/Math.cpp) (libc) - `lgamma` returns the host's `lgamma` and stores the sign of Γ(x) in the exported `signgam` (NID `5gOOC0kzW0c`, starting at 0 as in FreeBSD 11's [`s_signgam.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/s_signgam.c)) by the rules of [`e_lgamma_r.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/e_lgamma_r.c): -1 for -0 and for a negative non-integer whose floor is odd, 1 otherwise, including poles, infinities and NaN. Taken from the FreeBSD source, not measured on the console. The NID is in the aerolib symbol list; no title is known to read `signgam`. PPSA99004 imports `lgamma` @@ -126,7 +126,7 @@ - [%p and %lp](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) follow the console's `libSceLibcInternal.sprx` measured from a homebrew payload on firmware 12.70 ([#7549](https://github.com/boykopovar/AnyPS5/issues/7549)), not FreeBSD: bare lowercase hex with no `0x` prefix even under `#`, `0` for a null pointer, `+` and the space flag ignored, the width counting only the digits, `-` padding on the right, `0` padding with zeros when there is no precision, the precision as the minimum number of digits, nothing for a null pointer under `%.0p`, and `l` accepted. Only `snprintf` and `swprintf` of that module were measured, from a payload process rather than a title process, so a width beside `%.0p`, the `0` flag beside a precision (which C17 ignores) and `%.0p` of a non-null pointer follow this formatter's own rules and are not measured; `swprintf` was not run with `%lp`, so the wide family still refuses that conversion and returns -1. Linux routes `%p` and `%lp` to this formatter because glibc prints the FreeBSD form and `(nil)` for a null pointer, which means a format that combines `%p` with a conversion this formatter refuses, such as an argument position above 99, throws there instead of reaching glibc - [collate and collate _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build the facet for the C locale only and throw for any other locale name, the wide entry point following the narrow one it sits next to. The wide facet carries its own vtable, whose `compare`, `transform` and `hash` take the guest's 16-bit characters, so the length of a range is a count of characters: `compare` orders by 16-bit value (the C locale, as Dinkumware's `char_traits::compare` does), `transform` copies the characters into the result and records size and capacity in 16-bit elements (inline capacity 7, heap capacity `length | 7`, `(capacity + 1) * 2` bytes including the terminator), following Dinkumware's [VC12 `basic_string`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/xstring#L500-L531), and `hash` mixes one whole character per round, so wide text below 0x100 hashes like its narrow spelling and a character above 0xff hashes as one value instead of the two bytes it holds. The console's `libc.prx` exports both `_Getcat` (NIDs BSVJqITGCyI and 2wz4rthdiy8) with identical 0x19d-byte bodies, believed to build the same 0x20-byte facet; the console bodies themselves are unreadable (obfuscated dynsym, and the text of the relinked `libc.prx` is zeroed), so the wide entries follow the narrow algorithm and the C locale instead of the console's wide code, and no console run checks the two. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. The narrow result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware. No module of the PPSA59322, PPSA76677 or PPSA97358 dumps in this tree imports either `_Getcat`, so neither has been exercised by a title. - [std::__throw_bad_variant_access](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) throws `std::bad_variant_access` with the `what()` string `bad_variant_access`, the one [LLVM libc++](https://github.com/llvm/llvm-project/blob/main/libcxx/src/variant.cpp) returns, since the export carries libc++'s `__throw_bad_variant_access` name; the console's string is unverified. Onimusha: Way of the Sword (PPSA27836 01.000.001) imports it from `libc`, and its bundled `libc.prx` does not export it. -- [std::bad_optional_access and std::_Throw_bad_optional_access](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) provide the exception type, constructor, destructor, and `what()` exports alongside `_Throw_bad_optional_access`, returning the `what()` string `bad_optional_access` matching Dinkumware and libc++ conventions; the console libc.prx string is unverified. PPSA26578 (issue #7624) and PPSA34015 import `_ZSt26_Throw_bad_optional_accessv` (`KwzEip7m6TM`) from `libc`, and fail to load on main with unresolved import (0xC0000139). +- [std::bad_optional_access and std::_Throw_bad_optional_access](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) provide the exception type, constructor, destructor, and `what()` exports alongside `_Throw_bad_optional_access`, returning the `what()` string `bad_optional_access` matching Dinkumware and libc++ conventions; the console libc.prx string is unverified. PPSA26578 (issue #7624) and PPSA34015 import `_ZSt26_Throw_bad_optional_accessv` (`KwzEip7m6TM`) from `libc`. - [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, decodes the input of the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`) on Linux as UTF-8, mirroring the narrow printf family, where FreeBSD uses the locale's `mbrtowc` (one byte per character in the C locale); the console's locale encoding is unverified. Input that is not UTF-8, a character outside the BMP, a scanset member outside ASCII, positional arguments next to a wide conversion and wide conversions in `fscanf` throw. Each character is stored as one 16-bit unit, widths and `_s` sizes count characters, and a `%lc` field cut short by the end of the input keeps what it read, as FreeBSD's `convert_wchar` does. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits - [The scanf family](../../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` - [Guest path resolution](../../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation.. A relinked module's `.guest.prx` is matched the same way when the original module is not beside it: Borderlands 4 (PPSA01494) loads `/app0/prx/libRenoirCore.PS5.prx`, which it ships as `librenoircore.ps5.prx`. A relinked module request that misses both the exact and the case-insensitive `.guest.prx` takes the sibling whose stem (the name without its last extension, ASCII case ignored) equals the requested stem and that ends with `.guest.prx`, shortest name first: Unity IL2CPP titles load `Media/Modules/.prx` and `Media/Plugins/.prx`, and the relinker names the converted module after the ELF it was built from, `.elf.guest.prx`. The console's module namespace is assumed to ignore case and the extension; several stem matches pick the shortest name instead of throwing, which is not measured on a console From a809ad7d2fdf19bdab1e6463e34f6a66585a0d18 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:25:25 -0700 Subject: [PATCH 173/428] fix(libc): fail a q length in scanf as the console's libc does The console's own sscanf, run natively from three titles' libc.prx, returns 0 for %qd and stores nothing, where FreeBSD 11.4's source reads q as ll. A q directive now ends the scan there, and the count of conversions before it is returned. The test adds the console's rows for wrapping narrow conversions, saturating 64-bit ones with ERANGE, and %qd. --- .../libs/prx/libc/include/WindowsScanning.hpp | 9 +++++++-- core/libs/tests/Formatting.cpp | 19 +++++++++++++++---- docs/dev/debt/libc.md | 1 + 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsScanning.hpp b/core/libs/prx/libc/include/WindowsScanning.hpp index 420d8ba000..ab3f6ef625 100644 --- a/core/libs/prx/libc/include/WindowsScanning.hpp +++ b/core/libs/prx/libc/include/WindowsScanning.hpp @@ -32,11 +32,16 @@ inline int ScanWindowsArguments_nid_no_patch(const char* format, const void* sou translated += value; if (value != '%') continue; if (*format == '%') { translated += *format++; continue; } + const size_t directive = translated.size() - 1; const bool suppressed = *format == '*'; if (suppressed) translated += *format++; while (*format >= '0' && *format <= '9') translated += *format++; + if (*format == 'q') { + translated.resize(directive); + break; + } std::string length; - if (*format && std::strchr("hljztLq", *format)) { + if (*format && std::strchr("hljztL", *format)) { length += *format++; if ((length == "h" && *format == 'h') || (length == "l" && *format == 'l')) length += *format++; } @@ -48,7 +53,7 @@ inline int ScanWindowsArguments_nid_no_patch(const char* format, const void* sou } ++format; const bool narrow = !suppressed && std::strchr("diouxX", conversion) && (length.empty() || length == "h" || length == "hh"); - if (narrow || (std::strchr("diouxXn", conversion) && (length == "l" || length == "j" || length == "z" || length == "t" || length == "q"))) + if (narrow || (std::strchr("diouxXn", conversion) && (length == "l" || length == "j" || length == "z" || length == "t"))) translated += "ll"; else translated += length; translated += conversion; diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index 3b5f4e37c6..d8b89b7283 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -154,10 +154,21 @@ static void CheckIntegerScanWidths() { Require(sscanf_nid_postfix("200", "%hhu", &unsignedChar) == 1 && unsignedChar == 200); Require(sscanf_nid_postfix("300", "%hhu", &unsignedChar) == 1 && unsignedChar == 44); Require(sscanf_nid_postfix("377", "%hho", &unsignedChar) == 1 && unsignedChar == 255); - long long quad = 7; - Require(sscanf_nid_postfix("3000000000", "%qd", &quad) == 1 && quad == 3000000000ll); - unsigned long long unsignedQuad = 7; - Require(sscanf_nid_postfix("ffffffffff", "%qx", &unsignedQuad) == 1 && unsignedQuad == 0xffffffffffull); + *__error_nid_postfix() = 0; + Require(sscanf_nid_postfix("-2147483649", "%d", &value) == 1 && value == 0x7fffffff && *__error_nid_postfix() == 0); + Require(sscanf_nid_postfix("-1", "%u", &unsignedValue) == 1 && unsignedValue == 0xffffffffu && *__error_nid_postfix() == 0); + Require(sscanf_nid_postfix("99999999999", "%x", &unsignedValue) == 1 && unsignedValue == 0x99999999u && *__error_nid_postfix() == 0); + Require(sscanf_nid_postfix("70000", "%hd", &shortValue) == 1 && shortValue == 0x1170 && *__error_nid_postfix() == 0); + long long quad = 0x5555555555555555ll; + Require(sscanf_nid_postfix("9223372036854775808", "%lld", &quad) == 1 && quad == 0x7fffffffffffffffll && *__error_nid_postfix() == 34); + unsigned long long unsignedQuad = 0x5555555555555555ull; + *__error_nid_postfix() = 0; + Require(sscanf_nid_postfix("18446744073709551616", "%llu", &unsignedQuad) == 1 && unsignedQuad == 0xffffffffffffffffull && *__error_nid_postfix() == 34); +#ifdef _WIN32 + quad = 0x5555555555555555ll; + *__error_nid_postfix() = 0; + Require(sscanf_nid_postfix("5", "%qd", &quad) == 0 && quad == 0x5555555555555555ll && *__error_nid_postfix() == 0); +#endif int first = 7; int second = 7; int position = 0; diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 5014c4dd6f..047d2922cf 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -115,6 +115,7 @@ - [std::bad_optional_access and std::_Throw_bad_optional_access](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) provide the exception type, constructor, destructor, and `what()` exports alongside `_Throw_bad_optional_access`, returning the `what()` string `bad_optional_access` matching Dinkumware and libc++ conventions; the console libc.prx string is unverified. PPSA26578 (issue #7624) and PPSA34015 import `_ZSt26_Throw_bad_optional_accessv` (`KwzEip7m6TM`) from `libc`, and fail to load on main with unresolved import (0xC0000139). - [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, decodes the input of the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`) on Linux as UTF-8, mirroring the narrow printf family, where FreeBSD uses the locale's `mbrtowc` (one byte per character in the C locale); the console's locale encoding is unverified. Input that is not UTF-8, a character outside the BMP, a scanset member outside ASCII, positional arguments next to a wide conversion and wide conversions in `fscanf` throw. Each character is stored as one 16-bit unit, widths and `_s` sizes count characters, and a `%lc` field cut short by the end of the input keeps what it read, as FreeBSD's `convert_wchar` does. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits - [The scanf family](../../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` +- [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) on Linux reads a `q` length as `ll`, as glibc does. On Windows a `q` directive ends the scan with nothing stored, as the console's own libc does for `%qd`; FreeBSD 11.4's source reads it as `ll`. - [Guest path resolution](../../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation.. A relinked module's `.guest.prx` is matched the same way when the original module is not beside it: Borderlands 4 (PPSA01494) loads `/app0/prx/libRenoirCore.PS5.prx`, which it ships as `librenoircore.ps5.prx`. A relinked module request that misses both the exact and the case-insensitive `.guest.prx` takes the sibling whose stem (the name without its last extension, ASCII case ignored) equals the requested stem and that ends with `.guest.prx`, shortest name first: Unity IL2CPP titles load `Media/Modules/.prx` and `Media/Plugins/.prx`, and the relinker names the converted module after the ELF it was built from, `.elf.guest.prx`. The console's module namespace is assumed to ignore case and the extension; several stem matches pick the shortest name instead of throwing, which is not measured on a console - [Colons in guest names](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows (NTFS) a guest name containing `:` is not a plain file name: `name:stream` opens or creates the alternate data stream `stream` of the file `name`. Creating `/savedata0/C:file` makes an empty file `C` whose stream `file` holds the data, and `stat` of `/savedata0/C` then reports a regular file. `mkdir` of a name containing `:`, such as `C:` or `dir:x`, fails with EINVAL. `name:stream` already behaved this way before drive-letter components were kept under their mount - [tzname](../../../core/libs/prx/libc/src/Time.cpp) (libc) points at the host's zone names, read at load and again by `tzset`: the abbreviations a `TZ` string gives (`EST5EDT` is `EST` and `EDT` on both hosts), but on Windows without `TZ` the CRT's full names such as `GMT Standard Time`, which ICU's [uprv_tzname](https://github.com/unicode-org/icu/blob/457157a92aa053e632cc7fcfd0e12f8a943b2d11/icu4c/source/common/putil.cpp#L1244-L1264) (ES-DE in PPSA99169) cannot map to a zone ID. A zone with no daylight-saving time has `tzname[1]` empty on Windows and equal to `tzname[0]` on Linux, where FreeBSD leaves three spaces. The console's names are not known From 01e3c796da64bd7affd9d4c70b77af6a025b6c00 Mon Sep 17 00:00:00 2001 From: hyvvs Date: Sat, 24 Oct 2026 04:14:09 +0200 Subject: [PATCH 174/428] fix(ci): reject skips with mixed expected and unexpected reasons --- tools/check_skips.py | 9 +++++++-- tools/tests/test_check_skips.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/tools/check_skips.py b/tools/check_skips.py index d29c91b065..6ce7a7d47b 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -15,6 +15,11 @@ HIGH_REFRESH_LIMITATION = re.compile(r"current display does not provide 120 Hz", WINDOWS_PREFIX = "agc" +def device_limitations(output, patterns): + reasons = [line.strip() for line in output.splitlines() if line.strip().lower().startswith("skipped,")] + return bool(reasons) and all(any(pattern.search(reason) for pattern in patterns) for reason in reasons) + + def high_refresh_limitation(output): reasons = [] for line in output.splitlines(): @@ -63,10 +68,10 @@ def main(argv=None): print(f"{name}: {reason}") return 0 if args.job == "windows": - unexpected = [name for name, output in found if not name.startswith(WINDOWS_PREFIX) and not NO_DISPLAY.search(output) and not NO_VULKAN_DEVICE.search(output) and not high_refresh_limitation(output)] + unexpected = [name for name, output in found if not name.startswith(WINDOWS_PREFIX) and not device_limitations(output, (NO_DISPLAY, NO_VULKAN_DEVICE)) and not high_refresh_limitation(output)] expected = f"any {WINDOWS_PREFIX}* test, which the runner has no Vulkan driver for, or a test that reports no display or Vulkan device or a display without a 120 Hz mode" else: - unexpected = [name for name, output in found if not DEVICE_LIMITATION.search(output) and not high_refresh_limitation(output)] + unexpected = [name for name, output in found if not device_limitations(output, (DEVICE_LIMITATION,)) and not high_refresh_limitation(output)] expected = "any test that reports the device limitation it needs, such as narrow subgroups, a missing extension, no display for a Vulkan window or a display without a 120 Hz mode" for name in unexpected: print(f"error: {name} skipped") diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index 6e3871733b..02caf7fa3c 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -37,6 +37,37 @@ OTHER = [ class CheckSkipsTests(unittest.TestCase): + def test_unexpected_skip_reason_cannot_hide_behind_a_device_limitation(self): + reports = ( + ('skipped, the device lacks geometryShader', "linux"), + ('skipped, no display', "windows"), + ('flakyskipped, no usable Vulkan device', "windows"), + ('skipped, flaky\nskipped, the device lacks geometryShader', "linux"), + ) + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + for report, job in reports: + with self.subTest(job=job, report=report): + junit.write_text(f'{report}', encoding="utf-8") + result = subprocess.run([sys.executable, "-I", str(TOOL), str(junit), "--job", job], + capture_output=True, text=True) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("error: guest_probe skipped", result.stdout) + + def test_multiple_allowed_skip_reasons_remain_expected(self): + reports = ( + ('skipped, no display', "linux"), + ('skipped, no usable Vulkan device', "windows"), + ) + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + for report, job in reports: + with self.subTest(job=job, report=report): + junit.write_text(f'{report}', encoding="utf-8") + result = subprocess.run([sys.executable, "-I", str(TOOL), str(junit), "--job", job], + capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + def test_non_junit_xml_fails_in_every_cli_mode(self): reports = ('', 'coverage') with tempfile.TemporaryDirectory() as directory: From 0fed30252ac5732d7df491c9ac5131ba2cb4280b Mon Sep 17 00:00:00 2001 From: jacob6769 Date: Sat, 24 Oct 2026 12:01:56 +0900 Subject: [PATCH 175/428] fix(libSceLibreSsl): follow LibreSSL 2.5.4 and 4.0.0 for null digest buffers and SHA-512/t lengths Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceLibreSsl/src/Digest.cpp | 5 ++--- core/libs/prx/libSceLibreSsl3/src/Digest.cpp | 5 +++-- core/libs/tests/GuestLibreSsl3Digest.cpp | 10 +++++----- core/libs/tests/GuestLibreSslDigest.cpp | 14 +++++++++----- docs/dev/debt/libSceLibreSsl.md | 2 +- docs/dev/debt/libSceLibreSsl3.md | 2 +- 6 files changed, 21 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceLibreSsl/src/Digest.cpp b/core/libs/prx/libSceLibreSsl/src/Digest.cpp index e58974597e..d971a79478 100644 --- a/core/libs/prx/libSceLibreSsl/src/Digest.cpp +++ b/core/libs/prx/libSceLibreSsl/src/Digest.cpp @@ -340,8 +340,6 @@ int Sha512Final(unsigned char* digest, Sha512Context& c) { StoreBig64(buffer + 120, c.Nl); Sha512Block(c, buffer); if (!digest) return 0; - if (c.mdLength == Sha224Length || c.mdLength == Sha256Length) - NotImplemented_nid_no_patch("SHA512_Final: SHA-512/224 and SHA-512/256 digest lengths"); if (c.mdLength != Sha384Length && c.mdLength != Sha512Length) return 0; for (std::size_t i = 0; i < c.mdLength / 8; ++i) StoreBig64(digest + 8 * i, c.h[i]); return 1; @@ -490,8 +488,9 @@ int APS5_VABI SHA224_Final_nid_postfix(unsigned char* digest, Sha256Context* con } unsigned char* APS5_VABI SHA256_nid_postfix(const unsigned char* data, std::size_t length, unsigned char* digest) { + static unsigned char buffer[Sha256Length]; RequireInput(data, length, __func__); - if (!digest) NotImplemented_nid_no_patch("SHA256: null digest buffer"); + if (!digest) digest = buffer; Sha256Context c; SHA256_Init_nid_postfix(&c); if (length) Update32(c, data, length, Sha256Block); diff --git a/core/libs/prx/libSceLibreSsl3/src/Digest.cpp b/core/libs/prx/libSceLibreSsl3/src/Digest.cpp index 373654f3d5..ce7442c15e 100644 --- a/core/libs/prx/libSceLibreSsl3/src/Digest.cpp +++ b/core/libs/prx/libSceLibreSsl3/src/Digest.cpp @@ -1,4 +1,5 @@ #include +#include #include "prx/libc/include/General.hpp" extern "C" { @@ -10,7 +11,7 @@ int APS5_VABI SHA1_Update_nid_postfix(void* context, const void* data, std::size int APS5_VABI SHA1_Final_nid_postfix(unsigned char* digest, void* context); unsigned char* APS5_VABI MD5_nid_postfix(const unsigned char* data, std::size_t length, unsigned char* digest) { - if (!digest) NotImplemented_nid_no_patch("MD5: null digest buffer"); + if (!digest) throw std::invalid_argument("MD5: null digest buffer"); alignas(8) unsigned char context[92]; MD5_Init_nid_postfix(context); MD5_Update_nid_postfix(context, data, length); @@ -19,7 +20,7 @@ unsigned char* APS5_VABI MD5_nid_postfix(const unsigned char* data, std::size_t } unsigned char* APS5_VABI SHA1_nid_postfix(const unsigned char* data, std::size_t length, unsigned char* digest) { - if (!digest) NotImplemented_nid_no_patch("SHA1: null digest buffer"); + if (!digest) throw std::invalid_argument("SHA1: null digest buffer"); alignas(8) unsigned char context[96]; SHA1_Init_nid_postfix(context); SHA1_Update_nid_postfix(context, data, length); diff --git a/core/libs/tests/GuestLibreSsl3Digest.cpp b/core/libs/tests/GuestLibreSsl3Digest.cpp index 19f6f8cb2b..a952682f8a 100644 --- a/core/libs/tests/GuestLibreSsl3Digest.cpp +++ b/core/libs/tests/GuestLibreSsl3Digest.cpp @@ -18,8 +18,8 @@ static void Require(bool condition, std::source_location location = std::source_ std::abort(); } -template static void RequireThrows(TAction action, std::source_location location = std::source_location::current()) { - try { action(); } catch (const std::exception&) { return; } +template static void RequireInvalidArgument(TAction action, std::source_location location = std::source_location::current()) { + try { action(); } catch (const std::invalid_argument&) { return; } catch (const std::exception&) {} Require(false, location); } @@ -46,9 +46,9 @@ int main() { Require(SHA1_nid_postfix(abc, 3, digest) == digest && Hex(digest, 20) == "a9993e364706816aba3e25717850c26c9cd0d89d" && digest[20] == 0xee); Require(SHA1_nid_postfix(nullptr, 0, digest) == digest && Hex(digest, 20) == "da39a3ee5e6b4b0d3255bfef95601890afd80709"); Require(SHA1_nid_postfix(many, million.size(), digest) == digest && Hex(digest, 20) == "34aa973cd4c4daa4f61eeb2bdbad27316534016f"); - RequireThrows([&] { MD5_nid_postfix(abc, 3, nullptr); }); - RequireThrows([&] { SHA1_nid_postfix(abc, 3, nullptr); }); - RequireThrows([&] { SHA1_nid_postfix(nullptr, 1, digest); }); + RequireInvalidArgument([&] { MD5_nid_postfix(abc, 3, nullptr); }); + RequireInvalidArgument([&] { SHA1_nid_postfix(abc, 3, nullptr); }); + RequireInvalidArgument([&] { SHA1_nid_postfix(nullptr, 1, digest); }); std::puts("libSceLibreSsl3 digest tests passed"); } diff --git a/core/libs/tests/GuestLibreSslDigest.cpp b/core/libs/tests/GuestLibreSslDigest.cpp index 8b9ba6183b..aef45f5cf5 100644 --- a/core/libs/tests/GuestLibreSslDigest.cpp +++ b/core/libs/tests/GuestLibreSslDigest.cpp @@ -178,10 +178,12 @@ int main() { std::memcpy(context + 108, &length, 4); std::memset(digest, 0xee, sizeof(digest)); Require(SHA256_Final_nid_postfix(digest, context) == 1 && Hex(digest, 16) == "e3b0c44298fc1c149afbf4c8996fb924" && digest[16] == 0xee, "SHA256_Final truncates to the digest length"); - SHA512_Init_nid_postfix(context); - length = 32; - std::memcpy(context + 212, &length, 4); - RequireThrows([&] { SHA512_Final_nid_postfix(digest, context); }, "SHA-512/256 digest length"); + for (std::uint32_t truncated : {28u, 32u}) { + SHA512_Init_nid_postfix(context); + std::memcpy(context + 212, &truncated, 4); + std::memset(digest, 0xee, sizeof(digest)); + Require(SHA512_Final_nid_postfix(digest, context) == 0 && digest[0] == 0xee && digest[63] == 0xee, "SHA512_Final with a SHA-512/t digest length"); + } SHA256_Init_nid_postfix(context); length = 64; std::memcpy(context + 104, &length, 4); @@ -190,6 +192,8 @@ int main() { const auto* abc = reinterpret_cast("abc"); Require(SHA256_nid_postfix(abc, 3, digest) == digest && Hex(digest, 32) == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", "SHA256 one-shot"); Require(SHA256_nid_postfix(nullptr, 0, digest) == digest && Hex(digest, 32) == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "SHA256 one-shot of nothing"); - RequireThrows([&] { SHA256_nid_postfix(abc, 3, nullptr); }, "SHA256 one-shot without a digest buffer"); + unsigned char* shared = SHA256_nid_postfix(abc, 3, nullptr); + Require(shared && shared != digest && Hex(shared, 32) == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", "SHA256 one-shot into the static buffer"); + Require(SHA256_nid_postfix(nullptr, 0, nullptr) == shared && Hex(shared, 32) == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "SHA256 one-shot reuses the static buffer"); std::puts("libSceLibreSsl digest tests passed"); } diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index 29a95f22e3..fffa15a5fd 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -3,7 +3,7 @@ ### Unknown function info - [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 365 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. -- [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); the module's LibreSSL version is unknown and nothing is checked on the console. Behaviour that differs between LibreSSL versions throws: `SHA256` with a null digest buffer (older versions return a static buffer) and `SHA512_Final` with the SHA-512/224 or SHA-512/256 digest length. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw +- [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); nothing is checked on the console. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw - [DES_set_key, DES_ecb_encrypt, DES_set_odd_parity](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the 128-byte `DES_key_schedule` holds the 8-byte key followed by zeros instead of LibreSSL's precomputed subkeys, so a title reading its contents sees different bytes; `DES_ecb_encrypt` derives the subkeys through mbedtls on each call. Not compared with the console. Results follow LibreSSL [des_key.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des_key.c) and [des.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des.c): `DES_set_key` skips the parity and weak-key checks and returns 0, as it does while `DES_check_key` is 0 (that variable is not in the PS5 export inventory), a nonzero `enc` encrypts, and `DES_set_odd_parity` sets each byte's low bit for odd parity. Null pointers throw where LibreSSL faults - [X509_verify_cert_error_string](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - no console call is verified. The 64-bit argument, low-32-bit lookup, messages and shared numeric fallback follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509/x509_txt.c), the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware versions remain unverified. Unknown-code results share an overwritten static buffer and are not safe for concurrent use, as in that release; code 1 and codes above 54 use the numeric fallback. - [RAND_add, RAND_bytes, RAND_status, RAND_load_file, RAND_file_name, CRYPTO_malloc, CRYPTO_free, CRYPTO_set_mem_functions](../../../core/libs/prx/libSceLibreSsl/src/Runtime.cpp) (libSceLibreSsl) - follow LibreSSL's [rand_lib.c, randfile.c](https://github.com/libressl/openbsd/tree/0f0b16dc977f6ca499f0f4540078d992c96da597/src/lib/libcrypto/rand) and [malloc-wrapper.c](https://github.com/libressl/openbsd/blob/0f0b16dc977f6ca499f0f4540078d992c96da597/src/lib/libcrypto/malloc-wrapper.c), which are the same in 2.6.5 (3.4.3 linked); nothing is checked on the console. `RAND_bytes` reads host entropy and `CRYPTO_malloc` and `CRYPTO_free` use the guest heap. `CRYPTO_malloc` returns null for a size of zero or less, as LibreSSL 2.5.4 to 3.4 do; later versions take a `size_t` size and call `malloc`. A null buffer with a positive length throws diff --git a/docs/dev/debt/libSceLibreSsl3.md b/docs/dev/debt/libSceLibreSsl3.md index dba672f91f..c98952a914 100644 --- a/docs/dev/debt/libSceLibreSsl3.md +++ b/docs/dev/debt/libSceLibreSsl3.md @@ -7,4 +7,4 @@ ### Functional - [CRYPTO_memcmp](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) follows the public [LibreSSL signature and equality contract](https://man.openbsd.org/CRYPTO_memcmp.3), without PS5 verification. The exact nonzero result for unequal buffers is unspecified by that contract. Content-independent execution of its volatile byte reads still depends on the compiler; inspect optimized code when changing toolchains. Closes when: measured on a PS5 and the supported toolchains' generated code is checked. -- [MD5 and SHA1](../../../core/libs/prx/libSceLibreSsl3/src/Digest.cpp) (libSceLibreSsl3) - follow LibreSSL's [md5.c, sha1.c](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto) and hash through libSceLibreSsl's `MD5_*` and `SHA1_*`. A null digest buffer throws: LibreSSL up to 3.8 returns a static buffer, later versions do not check it. Nothing is checked on the console +- [MD5 and SHA1](../../../core/libs/prx/libSceLibreSsl3/src/Digest.cpp) (libSceLibreSsl3) - follow LibreSSL's [md5.c, sha1.c](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto) and hash through libSceLibreSsl's `MD5_*` and `SHA1_*`. A null digest buffer, which LibreSSL 4.0.0 does not check, throws. Nothing is checked on the console From 5b5dcaeaffb581483dabac1e61c85eb967caedc4 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Sat, 24 Oct 2026 09:33:06 +0700 Subject: [PATCH 176/428] fix(agc): keep a drawn linear target's results over a direct write's completion stamp A dispatch that writes a resident linear target's memory directly through a host import, and has completion work, stamps that memory a second time when its batch completes. With the target's draws stored at completion points, the completion can run inside the target's own store: FlushPending moves the target to the flushing list and then waits for the dispatch. StampKeepingNewerResults only kept block-tracked images still in the pending list, so the second stamp made every draw recorded after the dispatch look older than the dispatch, and the store dropped them all. The keep rule now also covers targets with drawn results and images that are already being flushed. Co-Authored-By: Claude --- .../libSceAgcDriver/Graphics/src/Texture.cpp | 10 +- .../LinearTargetDirectWriteCompletion.cpp | 351 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 3 files changed, 358 insertions(+), 4 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDirectWriteCompletion.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index d6ab97e9b2..e8902dc7cd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -4639,17 +4639,19 @@ std::uint64_t StorageTexture::StampKeepingNewerResults(std::uint64_t address, st if (recorded != 0 && bytes != 0) { auto& pending = Pending(); std::lock_guard lock(pending.mutex); - for (auto* texture : pending.textures) { - if (!texture->blockUnits || !texture->overlaps(address, bytes)) continue; + const auto consider = [&](StorageTexture* texture) { + if ((!texture->blockUnits && !texture->drawnListed) || !texture->overlaps(address, bytes)) return; auto alive = texture->weak_from_this().lock(); - if (alive == nullptr) continue; + if (alive == nullptr) return; for (std::uint32_t unit = 0; unit < texture->trackedLayers; ++unit) { const auto begin = texture->layerBegin(unit); const auto unitBytes = texture->layerBytes(unit); if (!texture->layerPending[unit] || begin >= address + bytes || address >= begin + unitBytes) continue; if (texture->layerGeneration[unit] >= recorded) kept.emplace_back(alive, unit); } - } + }; + for (auto* texture : pending.textures) consider(texture); + for (auto* texture : pending.flushing) consider(texture); } std::erase_if(kept, [](const auto& entry) { const auto& [texture, unit] = entry; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDirectWriteCompletion.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDirectWriteCompletion.cpp new file mode 100644 index 0000000000..ef4606f30a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDirectWriteCompletion.cpp @@ -0,0 +1,351 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Width = 256; +constexpr std::uint32_t Height = 128; +constexpr std::size_t Pitch = std::size_t{Width} * 4u; +constexpr std::size_t TargetBytes = Pitch * Height; +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t RegionBytes = 16 * BlockBytes; +constexpr std::size_t LabelOffset = 3 * BlockBytes; +constexpr std::uint32_t Untouched = 0x5a5a5a5au; +constexpr std::uint32_t Filled = 0x9abcdef0u; +constexpr std::uint32_t Drawn = 0xff0000ffu; +constexpr std::uint32_t LabelValue = 0x600dcafeu; +constexpr std::uint32_t Rgba8UnormInfo = 0x28028u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::array, 2> Scissors{{{16, 8, 128, 96}, {128, 32, 240, 120}}}; + +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060208u, 0x7e0802f2u, 0x7e0a02ffu, 0x44800000u, 0xf80000d1u, 0x00000005u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; +alignas(256) constexpr std::array FillCode{ + 0xd7460004, 0x04010c08, 0x7e000204, 0x7e020205, 0x7e040206, 0x7e060207, 0xe01c2000, 0x80000004, 0xbf810000, +}; +alignas(256) constexpr std::array PairFillCode{ + 0xd7460004, 0x04010c0c, 0x7e000204, 0x7e020205, 0x7e040206, 0x7e060207, 0xe01c2000, 0x80000004, 0xe01c2000, 0x80020004, 0xbf810000, +}; + +alignas(4) std::atomic Gate = 0; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; + +void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +void AppendDraw(std::vector& commands, std::uint64_t target, const std::array& scissor) { + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const auto extent = (Height << 16u) | Width; + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0x01200000}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, Rgba8UnormInfo}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, ((Width - 1u) << 14u) | (Height - 1u)}, + {0x3b8, LinearAttrib3}, {0x1e0, 0}, + {0xc, 0}, {0xd, extent}, + {0x81, 0x80000000}, {0x82, extent}, + {0x90, 0x80000000u | (scissor[1] << 16u) | scissor[0]}, {0x91, (scissor[3] << 16u) | scissor[2]}, + {0x94, 0x80000000}, {0x95, extent}, + {0x10f, std::bit_cast(2.0f * Width)}, {0x110, std::bit_cast(2.0f * Width)}, + {0x111, std::bit_cast(2.0f * Height)}, {0x112, std::bit_cast(2.0f * Height)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + {0x318, static_cast(target >> 8u)}, {0x390, static_cast(target >> 40u)}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 1u << 1u}, {0x8c, 0}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}, + }; + Append(commands, 0x79, {0x242, 4}); + Append(commands, 0x79, {0x24a, 0}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + Append(commands, 0x2d, {3, 2}); +} + +std::array BufferDescriptor(std::uint64_t address, std::size_t bytes) { + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(bytes / 16u), 0x4bfacu}; +} + +void AppendDispatch(std::vector& commands, const std::uint32_t* program, const std::vector& userData, std::uint32_t groups) { + const auto code = reinterpret_cast(program); + Append(commands, 0x76, {0x207, 64, 1, 1}); + Append(commands, 0x76, {0x20c, static_cast(code >> 8u), static_cast(code >> 40u)}); + Append(commands, 0x76, {0x213, (static_cast(userData.size()) << 1u) | 0x80u}); + commands.push_back(0xc0007600u | (static_cast(userData.size()) << 16u)); + commands.push_back(0x240u); + commands.insert(commands.end(), userData.begin(), userData.end()); + commands.insert(commands.end(), {0xc0031500u, groups, 1, 1, 0x41u}); +} + +void AppendFill(std::vector& commands, std::uint64_t target, const std::uint32_t* copied) { + const auto direct = BufferDescriptor(target, TargetBytes); + const auto other = BufferDescriptor(reinterpret_cast(copied), TargetBytes); + std::vector userData(direct.begin(), direct.end()); + userData.insert(userData.end(), {Filled, Filled, Filled, Filled}); + userData.insert(userData.end(), other.begin(), other.end()); + AppendDispatch(commands, PairFillCode.data(), userData, static_cast(TargetBytes / 1024u)); +} + +void AppendGate(std::vector& commands) { + const auto gate = reinterpret_cast(&Gate); + commands.insert(commands.end(), {0xc0053c00u, 0x13u, static_cast(gate), static_cast(gate >> 32u), 1u, 0xffffffffu, 0x19u}); +} + +void Submit(std::vector& commands) { + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); +} + +class Region { +public: + explicit Region(std::size_t bytes) : bytes(bytes), arena(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (arena) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + if (data != nullptr) GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (arena) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "linear target direct write completion: cannot map the guest region"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "linear target direct write completion: cannot allocate the guest region"); + auto* words = reinterpret_cast(data); + std::fill(words, words + bytes / 4u, Untouched); + GuestAllocations::Mutation().Add(data, bytes, true, true, true); + } + ~Region() { + GuestAllocations::Mutation().Remove(data); +#ifdef _WIN32 + if (arena) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (arena) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Region(const Region&) = delete; + Region& operator=(const Region&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::uint32_t Word(std::size_t offset) const { return std::atomic_ref(*reinterpret_cast(data + offset)).load(std::memory_order_acquire); } + std::size_t bytes; + +private: + bool arena; + std::uint8_t* data = nullptr; +}; + +void Import(const Region& region) { + const auto buffer = BufferDescriptor(region.Address(), region.bytes); + std::vector userData(buffer.begin(), buffer.end()); + userData.insert(userData.end(), {Untouched, Untouched, Untouched, Untouched}); + std::vector commands; + AppendDispatch(commands, FillCode.data(), userData, static_cast(region.bytes / 1024u)); + Submit(commands); + AgcDriverWaitIdle_nid_postfix(); +} + +bool Covered(std::uint32_t x, std::uint32_t y) { + return std::any_of(Scissors.begin(), Scissors.end(), [&](const std::array& scissor) { return x >= scissor[0] && x < scissor[2] && y >= scissor[1] && y < scissor[3]; }); +} + +std::string Mismatches(const Region& region, bool drawsLast) { + std::size_t covered = 0, wrongCovered = 0, filledCovered = 0, wrongElsewhere = 0, drawnElsewhere = 0; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto value = region.Word(y * Pitch + x * 4u); + if (drawsLast && Covered(x, y)) { + ++covered; + wrongCovered += value == Drawn ? 0u : 1u; + filledCovered += value == Filled ? 1u : 0u; + } else { + wrongElsewhere += value == Filled ? 0u : 1u; + drawnElsewhere += value == Drawn ? 1u : 0u; + } + } + } + if (wrongCovered == 0 && wrongElsewhere == 0) return {}; + const auto elsewhere = std::to_string(std::size_t{Width} * Height - covered); + if (!drawsLast) return std::to_string(wrongElsewhere) + " of the " + elsewhere + " pixels do not hold the fill's pattern (" + std::to_string(drawnElsewhere) + " hold the draws' color)"; + return std::to_string(wrongCovered) + " of the " + std::to_string(covered) + " pixels the draws covered do not hold their color (" + std::to_string(filledCovered) + " hold the fill's pattern) and " + std::to_string(wrongElsewhere) + " of the " + elsewhere + " pixels outside them do not hold the fill's pattern"; +} + +bool Await(const std::function& done) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (!done()) { + if (std::chrono::steady_clock::now() >= deadline) return false; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return true; +} + +enum class Order { FillFirst, DrawsFirst }; + +bool Run(Order order, const Region& region, std::vector& failures) { + const bool fillFirst = order == Order::FillFirst; + const char* name = fillFirst ? "fill, then draws" : "draws, then fill"; + const auto target = region.Address(); + Import(region); + if (!AgcDriver::GuestMemory::Watched(target, TargetBytes)) return false; + std::vector copied(TargetBytes / 4u + 4u, Untouched); + GuestAllocations::Mutation().Add(copied.data(), TargetBytes + 16u, true, true, true); + struct Unregister { + void* data; + ~Unregister() { GuestAllocations::Mutation().Remove(data); } + } unregister{copied.data()}; + Gate.store(0, std::memory_order_release); + std::vector commands; + if (fillFirst) AppendFill(commands, target, copied.data()); + for (const auto& scissor : Scissors) AppendDraw(commands, target, scissor); + if (!fillFirst) AppendFill(commands, target, copied.data()); + const auto label = target + LabelOffset; + commands.insert(commands.end(), {0xc0064900u, 0x514u, 1u << 29u, static_cast(label), static_cast(label >> 32u), LabelValue, 0, 0}); + if (fillFirst) AppendGate(commands); + Submit(commands); + std::string failure; + if (fillFirst) { + if (!Await([&] { return region.Word(LabelOffset) == LabelValue; })) failure = "the label after the draws never landed"; + else if (const auto mismatches = Mismatches(region, true); !mismatches.empty()) failure = "when the label after the draws landed: " + mismatches; + Gate.store(1, std::memory_order_release); + AgcDriverWaitIdle_nid_postfix(); + } else { + AgcDriverWaitIdle_nid_postfix(); + if (region.Word(LabelOffset) != LabelValue) failure = "the label after the fill had not landed by the device wait"; + else if (const auto mismatches = Mismatches(region, false); !mismatches.empty()) failure = "after the label and the device wait: " + mismatches; + } + AgcDriver::Graphics::StorageTexture::FlushPending(target, region.bytes, nullptr, "test release"); + std::printf("linear target direct write completion, %s: %s\n", name, failure.empty() ? "the later writer's values" : failure.c_str()); + if (!failure.empty()) failures.push_back(std::string(name) + ": " + failure); + return true; +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).hostImportAlignment == 0) { + std::puts("linear target direct write completion tests skipped, host imports are unavailable on this device, so the target is never resident"); + return VulkanTestSkipped; + } + } + std::vector> regions; + struct ShutdownGuard { + ~ShutdownGuard() { try { AgcDriverShutdown_nid_postfix(); } catch (...) {} } + } shutdown; + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + std::vector failures; + for (const auto order : {Order::FillFirst, Order::DrawsFirst}) { + regions.push_back(std::make_unique(RegionBytes)); + if (!Run(order, *regions.back(), failures)) { + std::puts("linear target direct write completion tests skipped, the device does not keep host imports write-watched, so no completion stamp over the target is tracked"); + return VulkanTestSkipped; + } + } + if (!failures.empty()) { + std::string message = "linear target direct write completion: at a completion point, the memory of a resident linear target did not hold the later of a direct buffer write with completion work and draws into it:"; + for (const auto& failure : failures) message += "\n " + failure; + Require(false, message); + } + AgcDriverShutdown_nid_postfix(); + std::puts("linear target direct write completion tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 648954a8e1..204d189fdb 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -115,6 +115,7 @@ - [Draw input snapshots](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`. - [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver), except drawn resident linear targets (next entry), are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. Explicit-size file writes (`write`, `pwrite`, `writev`, `pwritev`, AIO and `fwrite`) synchronize their source ranges through the [host-read hook](../../../core/libs/prx/libc/src/GuestArena.cpp); other host readers are not covered. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default. - [Resident linear color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver): a CPU store inside a drawn scissor rectangle, on a pixel no primitive wrote, made before the target's store is overwritten by it, where the console keeps it. Recorded draws' results are stored before the graphics queue's next flip, label, end-of-pipe interrupt, drain or device wait, as the union of their scissor rectangles; a draw that brings a 17th rectangle that does not merge stores the target at once. A target with an attachment proxy, several mips or layers, a 3D or DCC surface, or results of a dispatch, fill or copy is stored after each draw. One that a fill or copy writes after its draws is stored whole at the next of those points, and GPU results in its CPU-written 64 KiB blocks are dropped. A store the draw pipeline's committer makes (label, end-of-pipe interrupt, device wait) skips a target whose range a queued draw still reads (`DrawPipeline::HeldReads`). +- [Direct writes over a drawn linear target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver): the draws recorded after a dispatch that wrote the target's memory in place and has completion work are dropped in the dispatch's range when anything else, such as a CPU store elsewhere in the target, changed the target's memory since its generation, where the console keeps them. `StampKeepingNewerResults` checks a drawn target as one unit; closing the gap needs a generation per 64 KiB block. - [`APS5_SKIP_REFUSED_DRAWS`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp) (libSceAgcDriver) drops a draw whose graphics stage the recompiler refuses instead of failing the queue, so the frame lacks that draw, and the stage is compiled and refused again at each such draw. Compute refusals still fail. - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending saves its bytes through the unwatch hook, which also runs when a failed collect takes a range out of the watch. A range that leaves the watch without the hook running over it (an import fallback that unwatched without saving) leaves its pending results without saved bytes; storing such results throws. A unit whose results were stored while its blocks were watched keeps only a generation; when its blocks leave the watch while nothing is pending, the hook saves nothing, and results that land in it afterwards have neither saved bytes nor a watch, so storing them throws too. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory when the import probe keeps the write watch and a settle queue exists: those of 1 MiB or more stay watched, and smaller ones return to the watch once their settle has completed, unless a storage image overlaps them (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. From 847d3a1e19c8a50e37b48b40149779c4822c6079 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:07:34 -0700 Subject: [PATCH 177/428] test(agc): unwatch the heap copy ranges before checking their reasons Earlier tests in the same process can leave 0x10000+0x20000 or 0x40000+0x10000 in the Windows tracker's coverage, and AddressCopyOverflow names a heap's reason from GuestMemory::Watched over its range, so the precondition and the reasons depended on test order. Both ranges are unwatched first; the reasons checked after them are unchanged. --- core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index a476bdc4e0..788d9cb6c8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -3849,6 +3849,8 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { Require(AddressCopyOverflow({{0x1000, 0x3000, 0x2000, "uncommitted pages"}}, 0x2000).empty(), "copies within the limit were refused"); const auto copies = AddressCopyOverflow({{0x1000, 0x2000, 0x1000, "not mirrored"}, {0x10000, 0x30000, 0x18000, "uncommitted pages"}}, 0x2000); Require(!copies.empty() && copies.find("0x10000+0x20000 (0.1 MiB committed, uncommitted pages)") < copies.find("0x1000+0x1000"), "the copy limit does not name the largest copy first"); + AgcDriver::GuestMemory::Unwatch(0x10000, 0x20000); + AgcDriver::GuestMemory::Unwatch(0x40000, 0x10000); Require(!AgcDriver::GuestMemory::Watched(0x10000, 0x20000), "the unregistered heap copy range is write-watched"); const auto heapCopies = AddressCopyOverflow({{0x10000, 0x30000, 0x18000, nullptr, true, true}, {0x40000, 0x50000, 0x10000, nullptr, true, false}}, 0x2000); Require(heapCopies.find("0x10000+0x20000 (0.1 MiB committed, import over the host import budget, heap not fully write-watched)") != std::string::npos && heapCopies.find("0x40000+0x10000 (0.1 MiB committed, heap not fully write-watched)") != std::string::npos, "the copy limit does not name a copied heap's reason"); From 70e92a3537d6756ac677377f5168602aa570ee55 Mon Sep 17 00:00:00 2001 From: dotaku22 <64598464+dotaku22@users.noreply.github.com> Date: Sat, 24 Oct 2026 06:31:58 +0200 Subject: [PATCH 178/428] perf(agc): reuse uncommitted depth plane layers --- .../Graphics/src/DepthSurface.cpp | 7 +- .../tests/execution/DepthPlaneGuestLayers.cpp | 113 ++++++++++++++++++ 2 files changed, 118 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 458af384d5..3606328e9a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -1258,8 +1258,10 @@ public: continue; } surface = {}; - const GuestLayer current{resource.baseAddress + geometry.GuestLayerOffset(layer), geometry.layerBytes, resource.tileMode, geometry.thick, GuestMemory::CollectWrites(resource.baseAddress + geometry.GuestLayerOffset(layer), static_cast(geometry.layerBytes))}; - if (held.generation != 0 && held.address == current.address && held.bytes == current.bytes && held.tileMode == current.tileMode && held.thick == current.thick && !Recorder::SnapshotWriteOverlaps(current.address, static_cast(current.bytes)) && GuestMemory::UnchangedSince(current.address, static_cast(current.bytes), held.generation)) { + GuestLayer current{resource.baseAddress + geometry.GuestLayerOffset(layer), geometry.layerBytes, resource.tileMode, geometry.thick, GuestMemory::CollectWrites(resource.baseAddress + geometry.GuestLayerOffset(layer), static_cast(geometry.layerBytes))}; + current.uncommitted = current.generation == 0 && GuestMemory::CommittedRanges(current.address, static_cast(current.bytes)).empty(); + const bool unchanged = current.uncommitted ? held.uncommitted : held.generation != 0 && GuestMemory::UnchangedSince(current.address, static_cast(current.bytes), held.generation); + if (held.address == current.address && held.bytes == current.bytes && held.tileMode == current.tileMode && held.thick == current.thick && !Recorder::SnapshotWriteOverlaps(current.address, static_cast(current.bytes)) && unchanged) { held.generation = current.generation; continue; } @@ -1291,6 +1293,7 @@ private: TextureTileMode tileMode = TextureTileMode::kLinear; bool thick = false; std::uint64_t generation = 0; + bool uncommitted = false; }; VkImage image = VK_NULL_HANDLE; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp index db4b9a1841..f53341fd1b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp @@ -215,6 +215,65 @@ private: std::uint8_t* block = nullptr; }; +class SparseLayers { +public: + SparseLayers() { +#ifdef _WIN32 + memory = static_cast(GuestArena::GuestArenaAllocateAtOrAbove_nid_postfix(GuestArena::GuestArenaHighestFree_nid_no_patch(BlockBytes, 65536), BlockBytes, 65536)); +#else + constexpr std::uintptr_t alignment = 65536; + void* raw = mmap(nullptr, BlockBytes + alignment, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot reserve sparse depth layers"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + BlockBytes != begin + BlockBytes + alignment) munmap(reinterpret_cast(aligned + BlockBytes), begin + alignment - aligned); + memory = reinterpret_cast(aligned); +#endif + Require(memory != nullptr, "cannot reserve sparse depth layers"); + } + + ~SparseLayers() { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(memory, BlockBytes); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(), BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, BlockBytes); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, BlockBytes); + munmap(memory, BlockBytes); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(), BlockBytes); +#endif + } + + void Commit(std::size_t offset, std::size_t bytes) { +#ifdef _WIN32 + GuestArena::GuestArenaCommit_nid_postfix(memory + offset, bytes, PAGE_READWRITE, 65536); +#else + Require(mmap(memory + offset, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) != MAP_FAILED, "cannot commit sparse depth layer pages"); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(memory + offset, bytes); +#endif + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address() + offset, bytes); + } + + void DecommitGuestLayer() { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(memory + LayerBytes, LayerBytes); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory + LayerBytes, LayerBytes); + Require(mmap(memory + LayerBytes, LayerBytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) != MAP_FAILED, "cannot decommit sparse depth layer pages"); +#endif + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address() + LayerBytes, LayerBytes); + } + + std::uint8_t* Data() const { return memory; } + std::uint64_t Address() const { return reinterpret_cast(memory); } + SparseLayers(const SparseLayers&) = delete; + SparseLayers& operator=(const SparseLayers&) = delete; + +private: + std::uint8_t* memory = nullptr; +}; + std::atomic watchedBegin{0}; std::atomic watchedEnd{0}; std::atomic watchedReads{0}; @@ -300,6 +359,59 @@ void Run(const Context& context, const WatchedBlock& block) { AgcDriver::GuestMemory::SetFlushHook(nullptr); } +void SparseGuestLayers(const Context& context) { + ClearDepthSurfaces(context.device); + SparseLayers block; + block.Commit(0, LayerBytes); + FillLayer(block.Data(), 1.0f); + const auto address = block.Address(); + DepthSurfaceView(context, DepthTarget{address, 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}); + const auto resource = ArrayOver(address); + watchedBegin = address + LayerBytes; + watchedEnd = address + BlockBytes; + AgcDriver::GuestMemory::SetFlushHook(&CountReads); + const auto sample = [&](const char* when, bool changed) { + AgcDriver::GuestMemory::BumpCollectEpoch(); + watchedReads = 0; + Require(DepthSurfaceTexture(context, words, resource, components) != nullptr, "sparse depth layers were not sampled"); + Require((watchedReads.load() != 0) == changed, std::string("sparse depth layer read mismatch ") + when); + }; + try { + sample("at first use", true); + sample("with the guest layer still uncommitted", false); + block.Commit(LayerBytes, 65536); + const float value = 0.25f; + std::memcpy(block.Data() + LayerBytes, &value, sizeof(value)); + sample("after committing part of the guest layer", true); + block.Data()[LayerBytes + 64] = 1; + sample("after a CPU write to the committed part", true); + AgcDriver::GuestMemory::MarkWritten(address + LayerBytes + 128, 4); + sample("after a driver store to the committed part", true); + block.DecommitGuestLayer(); + sample("after decommitting the guest layer", true); + sample("with the guest layer still decommitted", false); + block.Commit(LayerBytes, 65536); + sample("after committing the same pages again", true); + block.DecommitGuestLayer(); + block.Commit(LayerBytes, 65536); + sample("after replacing a mapping with the same committed range", true); + block.DecommitGuestLayer(); + block.Commit(LayerBytes, LayerBytes); + FillLayer(block.Data() + LayerBytes, 0.5f); + sample("after fully committing the guest layer", true); + sample("with the fully committed layer unchanged", false); + block.DecommitGuestLayer(); + sample("after decommitting a fully committed guest layer", true); + sample("with the fully committed layer now empty", false); + } catch (...) { + AgcDriver::GuestMemory::SetFlushHook(nullptr); + ClearDepthSurfaces(context.device); + throw; + } + AgcDriver::GuestMemory::SetFlushHook(nullptr); + ClearDepthSurfaces(context.device); +} + void RecordedUploads(const Context& context, const WatchedBlock& block) { const auto resource = ArrayOver(block.Address()); Recorder recorder(context); @@ -410,6 +522,7 @@ int main() { RecordedUploads(context, block); RunCubeGather(context, block); RunCubeTiledSlice(context, block); + SparseGuestLayers(context); } device.reset(); std::puts("depth plane guest layer tests passed"); From da04d2eede88ce581be4d86b8f9e440160b72fc7 Mon Sep 17 00:00:00 2001 From: hypermist Date: Sat, 24 Oct 2026 18:11:40 +1300 Subject: [PATCH 179/428] feat(libSceFont): report font metrics through the FreeType driver sceFontGetFontMetrics fills the 64-byte metrics block through a new driver slot at SysDriver+0x98, as libSceFont and libSceFontFt do in firmware 13.60. Form 1 reads hhea, 2 the OS/2 typographic values, 3 the OS/2 Windows values and 4 the vertical values from vhea or OS/2; 2 and 3 fall back to hhea when OS/2 is missing or gives an empty range. The bounding box, em size, raw weight deltas and slant follow. A failed call clears the block and sets both weights to 1. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceFont/CMakeLists.txt | 9 ++ .../prx/libSceFont/include/FontDriver.hpp | 5 +- .../prx/libSceFont/include/FontInternal.hpp | 1 + .../libs/prx/libSceFont/include/FontTypes.hpp | 24 +++ core/libs/prx/libSceFont/src/Layout.cpp | 37 +++++ core/libs/prx/libSceFont/src/Render.cpp | 20 +++ .../libs/prx/libSceFont/src/Unimplemented.cpp | 6 - core/libs/prx/libSceFontFt/src/Driver.cpp | 105 ++++++++++++ core/libs/tests/GuestFontMetrics.cpp | 150 ++++++++++++++++++ docs/dev/debt/libSceFont.md | 3 +- 10 files changed, 352 insertions(+), 8 deletions(-) create mode 100644 core/libs/tests/GuestFontMetrics.cpp diff --git a/core/libs/prx/libSceFont/CMakeLists.txt b/core/libs/prx/libSceFont/CMakeLists.txt index cec16f879b..a3a3715934 100644 --- a/core/libs/prx/libSceFont/CMakeLists.txt +++ b/core/libs/prx/libSceFont/CMakeLists.txt @@ -38,4 +38,13 @@ if(BUILD_TESTING) set_tests_properties(font_face_lifetime font_face_lifetime_replace font_face_lifetime_churn PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_font_metrics_tests ${LIBS_INCLUDE_DIR}/tests/GuestFontMetrics.cpp) + target_include_directories(guest_font_metrics_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_font_metrics_tests PRIVATE libSceFont libSceFontFt libc) + configure_windows_unwind(guest_font_metrics_tests) + add_test(NAME guest_font_metrics COMMAND guest_font_metrics_tests) + if(WIN32) + set_tests_properties(guest_font_metrics PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceFont/include/FontDriver.hpp b/core/libs/prx/libSceFont/include/FontDriver.hpp index 35b8d3d533..cafdf3066b 100644 --- a/core/libs/prx/libSceFont/include/FontDriver.hpp +++ b/core/libs/prx/libSceFont/include/FontDriver.hpp @@ -208,6 +208,7 @@ using DriverGlyphsCountFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32 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 DriverFontMetricsFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t form, FontMetrics* outMetrics); 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); @@ -234,7 +235,8 @@ struct SysDriver { DriverGlyphIndexFunction glyph_index; DriverSetCharWithDpiFunction set_char_with_dpi; DriverSetCharDefaultDpiFunction set_char_default_dpi; - std::uint8_t reserved_0x90[0x10]; + std::uint8_t reserved_0x90[0x08]; + DriverFontMetricsFunction font_metrics; DriverComputeLayoutFunction compute_layout; DriverLoadGlyphCachedFunction load_glyph_cached; std::uint8_t reserved_0xb0[0x08]; @@ -343,6 +345,7 @@ 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, font_metrics) == 0x98); static_assert(offsetof(SysDriver, compute_layout) == 0xA0); static_assert(offsetof(SysDriver, get_glyph_metrics) == 0xB8); static_assert(offsetof(SysDriver, apply_glyph_adjust) == 0xE0); diff --git a/core/libs/prx/libSceFont/include/FontInternal.hpp b/core/libs/prx/libSceFont/include/FontInternal.hpp index 54c173835e..ed6ba622f7 100644 --- a/core/libs/prx/libSceFont/include/FontInternal.hpp +++ b/core/libs/prx/libSceFont/include/FontInternal.hpp @@ -133,6 +133,7 @@ int ComputeHorizontalLayout(FontHandle handle, const StyleStateBlock* style, std int ComputeVerticalLayout(FontHandle handle, const StyleStateBlock* style, std::uint8_t* outWords); int GetCharGlyphMetrics(FontHandle handle, std::uint32_t code, FontGlyphMetrics* metrics, bool useCachedStyle, float* usedScaleW = nullptr, float* usedScaleH = nullptr); int RenderCharGlyphImageCore(FontHandle handle, std::uint32_t code, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* result); +int ComputeFontMetrics(FontHandle handle, std::uint32_t form, FontMetrics* metrics); } diff --git a/core/libs/prx/libSceFont/include/FontTypes.hpp b/core/libs/prx/libSceFont/include/FontTypes.hpp index eb1a548bb4..8af95a1276 100644 --- a/core/libs/prx/libSceFont/include/FontTypes.hpp +++ b/core/libs/prx/libSceFont/include/FontTypes.hpp @@ -423,12 +423,36 @@ struct FontVerticalLayout { float decorationSpan; }; +struct FontMetrics { + std::uint32_t form; + float ascender; + float descender; + float lineGap; + float maxAdvance; + float verticalDescender; + float verticalAscender; + float xMin; + float yMin; + float xMax; + float yMax; + float emWidth; + float emHeight; + float effectWeightX; + float effectWeightY; + float slantRatio; +}; + 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(FontMetrics) == 0x40); +static_assert(offsetof(FontMetrics, maxAdvance) == 0x10); +static_assert(offsetof(FontMetrics, xMin) == 0x1C); +static_assert(offsetof(FontMetrics, emWidth) == 0x2C); +static_assert(offsetof(FontMetrics, effectWeightX) == 0x34); static_assert(sizeof(FontGlyphImageMetrics) == 0x18); static_assert(sizeof(FontGenerateGlyphDetail) == 0x20); static_assert(sizeof(FontResultStage) == 0x18); diff --git a/core/libs/prx/libSceFont/src/Layout.cpp b/core/libs/prx/libSceFont/src/Layout.cpp index 8bce6e00c2..60151a4cae 100644 --- a/core/libs/prx/libSceFont/src/Layout.cpp +++ b/core/libs/prx/libSceFont/src/Layout.cpp @@ -530,3 +530,40 @@ bool Font::BuildTrueOutline(GeneratedGlyph& glyph) { glyph.outlineInitialized = true; return true; } + +int Font::ComputeFontMetrics(FontHandle handle, std::uint32_t form, FontMetrics* metrics) { + auto* font = GetNativeFont(handle); + auto* library = font ? static_cast(font->library) : nullptr; + if (!library || !FontContext(library, font)) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + const SysDriver* driver = library->sys_driver; + if (!driver || !driver->set_char_with_dpi || !driver->set_char_default_dpi || !driver->font_metrics) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + const std::uint32_t fontId = font->open_info.ctx_entry_index; + if (static_cast(fontId) < 0) { + metrics->form = form; + metrics->ascender = 0.0f; + metrics->descender = 0.0f; + metrics->lineGap = 0.0f; + metrics->maxAdvance = 0.0f; + metrics->verticalDescender = 0.0f; + metrics->verticalAscender = 0.0f; + return SCE_FONT_OK; + } + const std::uint32_t modeLow = font->flags & 0x0Fu; + FontObj* head = nullptr; + std::uint32_t lockWord = 0; + auto* entry = AcquireFontCtxEntry(FontContext(library, font), fontId, modeLow, &head, &lockWord); + if (!entry) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + FontObj* match = FindSubFont(head, font->open_info.sub_font_index); + int rc = SCE_FONT_ERROR_FATAL; + if ((lockWord & OPEN_BIT) != 0 && (lockWord & COUNT_MASK) != 0 && match) { + float scaleX = font->style.scale_w; + float scaleY = font->style.scale_h; + match->font_handle = handle; + match->shift_units_x = 0; + match->shift_units_y = 0; + rc = SetDriverCharSize(driver, match, &font->style, scaleX, scaleY); + if (rc == SCE_FONT_OK) rc = driver->font_metrics(match, form, metrics); + } + ReleaseFontCtxEntryLock(entry, modeLow, lockWord); + return rc == SCE_FONT_OK ? rc : SCE_FONT_ERROR_INVALID_FONT_HANDLE; +} diff --git a/core/libs/prx/libSceFont/src/Render.cpp b/core/libs/prx/libSceFont/src/Render.cpp index 1f97674eb4..6d61ebb1b9 100644 --- a/core/libs/prx/libSceFont/src/Render.cpp +++ b/core/libs/prx/libSceFont/src/Render.cpp @@ -158,6 +158,26 @@ int APS5_VABI sceFontGetHorizontalLayout(FontHandle fontHandle, FontHorizontalLa return SCE_FONT_OK; } +int APS5_VABI sceFontGetFontMetrics(FontHandle fontHandle, std::uint32_t form, FontMetrics* metrics) { + auto* font = GetNativeFont(fontHandle); + std::uint32_t fontLock = 0; + if (font && font->magic == HANDLE_MAGIC && AcquireFontLock(font, fontLock)) { + if (!metrics) { + ReleaseFontLock(font, fontLock); + return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + } + const int rc = ComputeFontMetrics(fontHandle, form, metrics); + ReleaseFontLock(font, fontLock); + if (rc == SCE_FONT_OK) return rc; + } + if (metrics) { + *metrics = {}; + metrics->effectWeightX = 1.0f; + metrics->effectWeightY = 1.0f; + } + return SCE_FONT_ERROR_INVALID_FONT_HANDLE; +} + int APS5_VABI sceFontGetVerticalLayout(FontHandle fontHandle, FontVerticalLayout* layout) { int rc = SCE_FONT_ERROR_INVALID_FONT_HANDLE; auto* font = GetNativeFont(fontHandle); diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index c98c47a93e..4e44d99f9a 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -11,7 +11,6 @@ struct FontGraphicsServiceParam; struct FontWords; struct FontGlyphSupport; struct FontOutlineProfile; -struct FontMetrics; struct FontStyleInformation; struct FontGlyphExpandBufferState; struct FontGraphicsResource; @@ -184,11 +183,6 @@ int APS5_VABI sceFontGetFontGlyphsOutlineProfile(FontHandle font, FontOutlinePro return 0; } -int APS5_VABI sceFontGetFontMetrics(FontHandle font, std::uint32_t mode, FontMetrics* metrics) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontGetFontStyleInformation(FontHandle font, FontStyleInformation* information) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceFontFt/src/Driver.cpp b/core/libs/prx/libSceFontFt/src/Driver.cpp index a4561830ea..24fb3b07c6 100644 --- a/core/libs/prx/libSceFontFt/src/Driver.cpp +++ b/core/libs/prx/libSceFontFt/src/Driver.cpp @@ -777,6 +777,110 @@ std::uint64_t APS5_VABI RendererQuery(RendererNative* renderer, std::uint8_t* pa return 0; } +float ScaledUnits(std::int64_t units, std::int64_t fixed16) { + return FromF26Dot6(RoundMulFixed(units, fixed16)); +} + +void StoreVerticalFontMetrics(FT_Face face, const TT_OS2* os2, std::int64_t xShift, std::int64_t yShift, FontMetrics* metrics) { + const auto xScale = static_cast(face->size->metrics.x_scale); + const auto yScale = static_cast(face->size->metrics.y_scale); + const auto unitsPerEm = static_cast(static_cast(face->units_per_EM)); + const auto* vhea = static_cast(FT_Get_Sfnt_Table(face, FT_SFNT_VHEA)); + if (vhea) { + const std::int64_t minTop = vhea->min_Top_Side_Bearing; + metrics->ascender = ScaledUnits(minTop + yShift, yScale); + metrics->descender = ScaledUnits(static_cast(vhea->yMax_Extent) + yShift, yScale); + metrics->lineGap = 0.0f; + if (vhea->Version >= 0x110000 && vhea->Line_Gap != 0) { + metrics->lineGap = FromF26Dot6(RoundMulFixed(vhea->Line_Gap, xScale) - TruncFixed(unitsPerEm * xScale)); + } + metrics->verticalDescender = ScaledUnits(static_cast(vhea->Descender) + xShift, xScale); + const std::int64_t ascender = os2 ? os2->sTypoAscender : static_cast(minTop + face->bbox.yMax); + metrics->verticalAscender = ScaledUnits(ascender + yShift, yScale); + } else { + std::int64_t ascender = 0; + if (os2) { + ascender = os2->sTypoAscender; + metrics->ascender = ScaledUnits(ascender - face->bbox.yMax, yScale); + metrics->descender = ScaledUnits(ascender - face->bbox.yMin, yScale); + metrics->lineGap = 0.0f; + } else { + const std::int32_t lineGap = face->height - face->ascender + face->descender; + const std::int32_t halfGap = lineGap / 2; + metrics->ascender = ScaledUnits(halfGap, yScale); + metrics->descender = ScaledUnits(face->ascender - face->descender + halfGap, yScale); + metrics->lineGap = ScaledUnits(lineGap, xScale); + ascender = face->ascender + halfGap; + } + metrics->verticalAscender = ScaledUnits(ascender + yShift, yScale); + metrics->verticalDescender = ScaledUnits(-static_cast(unitsPerEm / 2), xScale); + } + metrics->maxAdvance = ScaledUnits(face->max_advance_height, yScale); +} + +std::uint32_t StoreHorizontalFontMetrics(FT_Face face, const TT_OS2* os2, std::uint32_t form, std::int64_t xShift, std::int64_t yShift, FontMetrics* metrics) { + const auto xScale = static_cast(face->size->metrics.x_scale); + const auto yScale = static_cast(face->size->metrics.y_scale); + const auto* hhea = static_cast(FT_Get_Sfnt_Table(face, FT_SFNT_HHEA)); + metrics->verticalDescender = 0.0f; + metrics->verticalAscender = 0.0f; + bool fromHorizontalHeader = form == 1 || ((form == 2 || form == 3) && !os2); + if (form == 2 && os2) { + metrics->ascender = ScaledUnits(static_cast(os2->sTypoAscender) + yShift, yScale); + metrics->descender = ScaledUnits(static_cast(os2->sTypoDescender) + yShift, yScale); + metrics->lineGap = ScaledUnits(os2->sTypoLineGap, yScale); + fromHorizontalHeader = metrics->ascender == 0.0f && metrics->descender == 0.0f; + } else if (form == 3 && os2) { + metrics->ascender = ScaledUnits(static_cast(os2->usWinAscent) + yShift, yScale); + metrics->descender = FromF26Dot6(-RoundMulFixed(static_cast(os2->usWinDescent) - yShift, yScale)); + metrics->lineGap = hhea ? ScaledUnits(hhea->Line_Gap, yScale) : 0.0f; + fromHorizontalHeader = metrics->ascender == metrics->descender; + } + if (fromHorizontalHeader) { + form = 1; + if (hhea) { + metrics->ascender = ScaledUnits(static_cast(hhea->Ascender) + yShift, yScale); + metrics->descender = ScaledUnits(static_cast(hhea->Descender) + yShift, yScale); + metrics->lineGap = ScaledUnits(hhea->Line_Gap, yScale); + } else { + metrics->ascender = ScaledUnits(static_cast(face->ascender) + yShift, yScale); + metrics->descender = ScaledUnits(static_cast(face->descender) + yShift, yScale); + metrics->lineGap = 0.0f; + } + } + metrics->maxAdvance = ScaledUnits(static_cast(face->max_advance_width) + xShift, xScale); + return form; +} + +int APS5_VABI LibraryGetFontMetrics(FontObj* obj, std::uint32_t form, FontMetrics* metrics) { + const FT_Face face = obj ? FaceOf(obj) : nullptr; + if (!face || !face->size || !metrics) 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); + const std::int64_t xShift = obj->shift_cache_x; + const std::int64_t yShift = obj->shift_cache_y; + const auto* os2 = static_cast(FT_Get_Sfnt_Table(face, FT_SFNT_OS2)); + if (os2 && os2->version == 0xFFFF) os2 = nullptr; + if (form == 4) { + StoreVerticalFontMetrics(face, os2, xShift, yShift, metrics); + } else { + form = StoreHorizontalFontMetrics(face, os2, form, xShift, yShift, metrics); + } + metrics->xMin = ScaledUnits(static_cast(face->bbox.xMin) + xShift, xScale); + metrics->yMin = ScaledUnits(static_cast(face->bbox.yMin) + yShift, yScale); + metrics->xMax = ScaledUnits(static_cast(face->bbox.xMax) + xShift, xScale); + metrics->yMax = ScaledUnits(static_cast(face->bbox.yMax) + yShift, yScale); + const auto unitsPerEm = static_cast(static_cast(face->units_per_EM)); + metrics->emWidth = FromF26Dot6(TruncFixed(unitsPerEm * xScale)); + metrics->emHeight = FromF26Dot6(TruncFixed(unitsPerEm * yScale)); + const auto* handle = reinterpret_cast(obj->font_handle); + metrics->effectWeightX = handle ? handle->style.effect_weight_x : 0.0f; + metrics->effectWeightY = handle ? handle->style.effect_weight_y : 0.0f; + metrics->slantRatio = handle ? handle->style.slant_ratio : 0.0f; + metrics->form = form; + return SCE_FONT_OK; +} + SysDriver MakeDriverTable() { SysDriver driver{}; driver.pixel_resolution = &LibraryGetPixelResolution; @@ -791,6 +895,7 @@ SysDriver MakeDriverTable() { driver.glyph_index = &LibraryGetGlyphIndex; driver.set_char_with_dpi = &LibrarySetCharSizeWithDpi; driver.set_char_default_dpi = &LibrarySetCharSizeDefaultDpi; + driver.font_metrics = &LibraryGetFontMetrics; driver.compute_layout = &LibraryComputeLayout; driver.compute_layout_alt = &LibraryComputeLayoutAlt; driver.load_glyph_cached = &LibraryLoadGlyphCached; diff --git a/core/libs/tests/GuestFontMetrics.cpp b/core/libs/tests/GuestFontMetrics.cpp new file mode 100644 index 0000000000..5f77094ef2 --- /dev/null +++ b/core/libs/tests/GuestFontMetrics.cpp @@ -0,0 +1,150 @@ +#include "prx/libSceFont/include/FontDriver.hpp" +#include "tests/FontFixture.hpp" +#include +#include +#include +#include +#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 sceFontSupportExternalFonts(FontLibrary, std::uint32_t, std::uint32_t); +int APS5_VABI sceFontOpenFontMemory(FontLibrary, const void*, std::uint32_t, const FontOpenDetail*, FontHandle*); +int APS5_VABI sceFontCloseFont(FontHandle); +int APS5_VABI sceFontSetScalePixel(FontHandle, float, float); +int APS5_VABI sceFontSetEffectWeight(FontHandle, float, float, std::uint32_t); +int APS5_VABI sceFontSetEffectSlant(FontHandle, float); +int APS5_VABI sceFontGetFontMetrics(FontHandle, std::uint32_t, FontMetrics*); +const void* APS5_VABI sceFontSelectLibraryFt(int); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Font metrics check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +struct Expected { + std::uint32_t form; + float ascender; + float descender; + float lineGap; + float maxAdvance; + float verticalDescender; + float verticalAscender; +}; + +static FontMetrics Metrics(FontHandle font, std::uint32_t form) { + FontMetrics metrics{}; + std::memset(&metrics, 0x7F, sizeof(metrics)); + Require(sceFontGetFontMetrics(font, form, &metrics) == SCE_FONT_OK); + return metrics; +} + +static void RequireMetrics(FontHandle font, std::uint32_t form, const Expected& expected) { + const FontMetrics metrics = Metrics(font, form); + Require(metrics.form == expected.form); + Require(metrics.ascender == expected.ascender && metrics.descender == expected.descender && metrics.lineGap == expected.lineGap); + Require(metrics.maxAdvance == expected.maxAdvance); + Require(metrics.verticalDescender == expected.verticalDescender && metrics.verticalAscender == expected.verticalAscender); + Require(metrics.xMin == 0.0f && metrics.yMin == 0.0f && metrics.xMax == 0.0f && metrics.yMax == 0.0f); + Require(metrics.emWidth == 99.984375f && metrics.emHeight == 99.984375f); +} + +static void* APS5_VABI Allocate(void*, std::uint32_t size) { + return std::malloc(size); +} + +static void APS5_VABI Release(void*, void* pointer) { + std::free(pointer); +} + +using FontFixture::BuildFont; +using FontFixture::Words; + +static std::vector Os2Table() { + return Words({0, 500, 400, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0x20, 0x7E, 700, -300, 100, 900, 250}); +} + +static FontHandle Open(FontLibrary library, const std::vector& data) { + FontHandle font = nullptr; + Require(sceFontOpenFontMemory(library, data.data(), static_cast(data.size()), nullptr, &font) == SCE_FONT_OK && font != nullptr); + Require(sceFontSetScalePixel(font, 100.0f, 100.0f) == SCE_FONT_OK); + return font; +} + +static std::vector VerticalHeader(int major) { + return Words({major, 0, 880, -120, 60, 1000, 40, 0, 950, 0, 0, 0, 0, 0, 0, 0, 0, 1}); +} + +int main() { + const FontMemoryInterface iface{Allocate, Release, nullptr, nullptr, nullptr, nullptr}; + FontMemory memory{}; + Require(sceFontMemoryInit(&memory, nullptr, 0, &iface, nullptr, nullptr, nullptr) == SCE_FONT_OK); + FontLibrary library = nullptr; + Require(sceFontCreateLibrary(&memory, sceFontSelectLibraryFt(0), &library) == SCE_FONT_OK); + Require(sceFontSupportExternalFonts(library, 4, 0x52) == SCE_FONT_OK); + + const auto plainData = BuildFont(1, {}); + FontHandle plain = Open(library, plainData); + RequireMetrics(plain, 1, {1, 80.0f, -20.0f, 0.0f, 50.0f, 0.0f, 0.0f}); + RequireMetrics(plain, 2, {1, 80.0f, -20.0f, 0.0f, 50.0f, 0.0f, 0.0f}); + RequireMetrics(plain, 3, {1, 80.0f, -20.0f, 0.0f, 50.0f, 0.0f, 0.0f}); + RequireMetrics(plain, 4, {4, 0.0f, 100.0f, 0.0f, 100.0f, -50.0f, 80.0f}); + FontMetrics untouched{}; + untouched.ascender = 5.0f; + untouched.descender = 6.0f; + untouched.lineGap = 7.0f; + Require(sceFontGetFontMetrics(plain, 7, &untouched) == SCE_FONT_OK); + Require(untouched.form == 7 && untouched.ascender == 5.0f && untouched.descender == 6.0f && untouched.lineGap == 7.0f); + Require(untouched.maxAdvance == 50.0f && untouched.verticalDescender == 0.0f && untouched.verticalAscender == 0.0f); + + const auto typoData = BuildFont(1, {{"OS/2", Os2Table()}, {"vhea", VerticalHeader(0x11)}, {"vmtx", Words({1000, 40})}}); + FontHandle typo = Open(library, typoData); + RequireMetrics(typo, 1, {1, 80.0f, -20.0f, 0.0f, 50.0f, 0.0f, 0.0f}); + RequireMetrics(typo, 2, {2, 70.0f, -30.0f, 10.0f, 50.0f, 0.0f, 0.0f}); + RequireMetrics(typo, 3, {3, 90.0f, -25.0f, 0.0f, 50.0f, 0.0f, 0.0f}); + RequireMetrics(typo, 4, {4, 4.0f, 95.0f, -93.984375f, 100.0f, -12.0f, 70.0f}); + Require(sceFontSetEffectWeight(typo, 1.03125f, 0.984375f, 0) == SCE_FONT_OK); + Require(sceFontSetEffectSlant(typo, 0.25f) == SCE_FONT_OK); + const FontMetrics styled = Metrics(typo, 2); + Require(styled.effectWeightX == 0.03125f && styled.effectWeightY == -0.015625f && styled.slantRatio == 0.25f); + Require(sceFontSetEffectWeight(typo, 1.5f, 0.5f, 0) == SCE_FONT_OK); + const FontMetrics clamped = Metrics(typo, 2); + Require(clamped.effectWeightX == 0.04f && clamped.effectWeightY == -0.04f); + + const auto versionOneData = BuildFont(1, {{"vhea", VerticalHeader(1)}, {"vmtx", Words({1000, 40})}}); + FontHandle versionOne = Open(library, versionOneData); + RequireMetrics(versionOne, 4, {4, 4.0f, 95.0f, 0.0f, 100.0f, -12.0f, 4.0f}); + + const auto os2OnlyData = BuildFont(1, {{"OS/2", Os2Table()}}); + FontHandle os2Only = Open(library, os2OnlyData); + RequireMetrics(os2Only, 4, {4, 70.0f, 70.0f, 0.0f, 100.0f, -50.0f, 70.0f}); + + FontMetrics cleared{}; + std::memset(&cleared, 0x7F, sizeof(cleared)); + Require(sceFontGetFontMetrics(nullptr, 1, &cleared) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(cleared.form == 0 && cleared.ascender == 0.0f && cleared.maxAdvance == 0.0f && cleared.emHeight == 0.0f); + Require(cleared.effectWeightX == 1.0f && cleared.effectWeightY == 1.0f && cleared.slantRatio == 0.0f); + std::uint64_t notAFont[32]{}; + std::memset(&cleared, 0x7F, sizeof(cleared)); + Require(sceFontGetFontMetrics(reinterpret_cast(notAFont), 1, &cleared) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(cleared.form == 0 && cleared.effectWeightX == 1.0f); + Require(sceFontGetFontMetrics(plain, 1, nullptr) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(sceFontGetFontMetrics(nullptr, 1, nullptr) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + + for (FontHandle font : {plain, typo, versionOne, os2Only}) Require(sceFontCloseFont(font) == SCE_FONT_OK); + Require(sceFontDestroyLibrary(&library) == SCE_FONT_OK); + Require(sceFontMemoryTerm(&memory) == SCE_FONT_OK); + std::puts("Font metrics tests passed"); + return 0; +} diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index 3158492e1f..c59aa9fb2b 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -22,9 +22,10 @@ - [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - the other script files of a set are chosen from its type, not measured on a console: Vietnamese for low-byte bit 0x10, Thai for script bit 0x10, Japanese for 0x04, simplified Chinese for 0x80 (before Japanese with 0x08), Hangul for 0x20 and Arabic for low-byte bit 0x80, as the SDK set names (`..._JP_AR`, `..._GBCJK_TH`) listed by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/font/font_internal.cpp) suggest; the file order and the Hangul file name (`SCEPS4Yoongd-Medium.otf`, `SCEPS4Yoongd-Bold.otf`) also come from shadPS4. A character the primary file lacks is taken from the first of them that has it; kerning between two files is 0, and `sceFontGetCharGlyphCode` and `sceFontGetFontGlyphsCount` answer for the primary file only - [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - the console file names are those of the PS4 firmware; the names of the PS5-only JG2 sets (`0x1A......`) are unknown, so they use the SSTJpPro files. When a console file is missing, an openly licensed substitute (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans Arabic, Noto Sans CJK with its Korean face for Hangul) is loaded instead; its metrics differ, so text width, line height and wrapping differ from the console. System font faces are loaded by FreeType in host memory, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. - [sceFontGetCharGlyphMetrics, sceFontGetRenderCharGlyphMetrics](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) report the vertical metrics the way the [FreeType driver](../../../core/libs/prx/libSceFontFt/src/Driver.cpp) reports them for a rendered glyph, not confirmed on a title: FreeType's vertical advance, and for a face without vertical metrics a bearing of half the horizontal advance left of the horizontal bearing and of half the advance left over by the glyph height. +- [sceFontGetFontMetrics](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) - the 64-byte output, the form fallbacks (2 and 3 fall back to the `hhea` values when OS/2 is missing or gives an empty range, 4 builds vertical values from `vhea` or OS/2) and the raw weight deltas and slant copied from the style are read from the disassembly of firmware 13.60 `libSceFont.sprx` and `libSceFontFt.sprx`, not measured on a console. For a font set the values are those of its primary file; which file the console reports is not recovered. Closes when: measured on a PS5 - [Font writing lines](../../../core/libs/prx/libSceFont/src/WritingLine.cpp): visual ordering and spacing originated from PS4 measurements and match the supplied PS5 structure and control flow. Exhaustive floating-point equivalence, including NaN payloads and rounding, has not been verified against a running PS5 module. -- [Font graphics and metric signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): graphics-device/service creation takes font memory, parameters and a handle output; destruction takes a handle pointer. Attribute selectors and outputs are 32-bit values. Metric, outline-profile, style-information and expansion-buffer outputs remain opaque pending field recovery. Protected implementations are unchanged. +- [Font graphics and metric signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): graphics-device/service creation takes font memory, parameters and a handle output; destruction takes a handle pointer. Attribute selectors and outputs are 32-bit values. Outline-profile, style-information and expansion-buffer outputs remain opaque pending field recovery. Protected implementations are unchanged. - [Font graphics fill and transform signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): region layout and scalar float arguments are confirmed from assembly, including the extra 32-bit corner flags omitted by the roundish-region decompilation. Fill-method, plot and rates layouts, layout selector meanings and mapping parameter meanings remain unresolved. Protected bodies are unchanged. From 3880821a125d828c96a78ca01147a2c5e80641d9 Mon Sep 17 00:00:00 2001 From: hypermist Date: Sat, 24 Oct 2026 18:16:10 +1300 Subject: [PATCH 180/428] test(libSceFont): register the font metrics test away from other open PRs --- core/libs/prx/libSceFont/CMakeLists.txt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceFont/CMakeLists.txt b/core/libs/prx/libSceFont/CMakeLists.txt index a3a3715934..1327eded0a 100644 --- a/core/libs/prx/libSceFont/CMakeLists.txt +++ b/core/libs/prx/libSceFont/CMakeLists.txt @@ -16,6 +16,15 @@ if(BUILD_TESTING) set_tests_properties(guest_font guest_system_font PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_font_metrics_tests ${LIBS_INCLUDE_DIR}/tests/GuestFontMetrics.cpp) + target_include_directories(guest_font_metrics_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_font_metrics_tests PRIVATE libSceFont libSceFontFt libc) + configure_windows_unwind(guest_font_metrics_tests) + add_test(NAME guest_font_metrics COMMAND guest_font_metrics_tests) + if(WIN32) + set_tests_properties(guest_font_metrics PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_font_writing_line_tests ${LIBS_INCLUDE_DIR}/tests/GuestFontWritingLine.cpp) target_include_directories(guest_font_writing_line_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_font_writing_line_tests PRIVATE libSceFont libc) @@ -38,13 +47,4 @@ if(BUILD_TESTING) set_tests_properties(font_face_lifetime font_face_lifetime_replace font_face_lifetime_churn PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - add_test_executable(guest_font_metrics_tests ${LIBS_INCLUDE_DIR}/tests/GuestFontMetrics.cpp) - target_include_directories(guest_font_metrics_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_font_metrics_tests PRIVATE libSceFont libSceFontFt libc) - configure_windows_unwind(guest_font_metrics_tests) - add_test(NAME guest_font_metrics COMMAND guest_font_metrics_tests) - if(WIN32) - set_tests_properties(guest_font_metrics PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") - endif() endif() From 2dd8d4902ddbca670bd687f5a139f12799fd216d Mon Sep 17 00:00:00 2001 From: hypermist Date: Sat, 24 Oct 2026 18:30:38 +1300 Subject: [PATCH 181/428] feat(libSceFont): report font style information through the FreeType driver sceFontGetFontStyleInformation fills the 24-byte style block through the driver slot at SysDriver+0x48, as libSceFont and libSceFontFt do in firmware 13.60: the FreeType family and style names, the italic and bold flags, the OS/2 weight class when it is a multiple of 100 from 100 to 900, the width class below 10, and 1 plus a right-to-left head direction plus 0x80 for vertical metrics. A font-set record supplies the same fields directly. A failed call points both names at an empty string and clears the rest. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceFont/CMakeLists.txt | 9 + .../prx/libSceFont/include/FontDriver.hpp | 4 +- .../prx/libSceFont/include/FontInternal.hpp | 1 + .../libs/prx/libSceFont/include/FontTypes.hpp | 11 ++ core/libs/prx/libSceFont/src/Layout.cpp | 33 ++++ core/libs/prx/libSceFont/src/Render.cpp | 20 +++ .../libs/prx/libSceFont/src/Unimplemented.cpp | 6 - core/libs/prx/libSceFontFt/src/Driver.cpp | 21 +++ core/libs/tests/GuestFontStyleInformation.cpp | 168 ++++++++++++++++++ docs/dev/debt/libSceFont.md | 3 +- 10 files changed, 268 insertions(+), 8 deletions(-) create mode 100644 core/libs/tests/GuestFontStyleInformation.cpp diff --git a/core/libs/prx/libSceFont/CMakeLists.txt b/core/libs/prx/libSceFont/CMakeLists.txt index 1327eded0a..3e9576b964 100644 --- a/core/libs/prx/libSceFont/CMakeLists.txt +++ b/core/libs/prx/libSceFont/CMakeLists.txt @@ -25,6 +25,15 @@ if(BUILD_TESTING) set_tests_properties(guest_font_metrics PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_font_style_information_tests ${LIBS_INCLUDE_DIR}/tests/GuestFontStyleInformation.cpp) + target_include_directories(guest_font_style_information_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_font_style_information_tests PRIVATE libSceFont libSceFontFt libc) + configure_windows_unwind(guest_font_style_information_tests) + add_test(NAME guest_font_style_information COMMAND guest_font_style_information_tests) + if(WIN32) + set_tests_properties(guest_font_style_information PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_font_writing_line_tests ${LIBS_INCLUDE_DIR}/tests/GuestFontWritingLine.cpp) target_include_directories(guest_font_writing_line_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_font_writing_line_tests PRIVATE libSceFont libc) diff --git a/core/libs/prx/libSceFont/include/FontDriver.hpp b/core/libs/prx/libSceFont/include/FontDriver.hpp index cafdf3066b..d766c4209f 100644 --- a/core/libs/prx/libSceFont/include/FontDriver.hpp +++ b/core/libs/prx/libSceFont/include/FontDriver.hpp @@ -209,6 +209,7 @@ using DriverGlyphIndexFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_ 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 DriverFontMetricsFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t form, FontMetrics* outMetrics); +using DriverStyleInformationFunction = int (APS5_VABI*)(FontObj* fontObj, FontStyleInformation* outInformation); 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); @@ -226,7 +227,7 @@ struct SysDriver { std::uint8_t reserved_0x30[0x08]; DriverOpenFunction open; DriverCloseFunction close; - std::uint8_t reserved_0x48[0x08]; + DriverStyleInformationFunction style_information; DriverScaleFunction scale; std::uint8_t reserved_0x58[0x08]; DriverMetricFunction metric; @@ -343,6 +344,7 @@ 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, style_information) == 0x48); static_assert(offsetof(SysDriver, scale) == 0x50); static_assert(offsetof(SysDriver, glyph_index) == 0x78); static_assert(offsetof(SysDriver, font_metrics) == 0x98); diff --git a/core/libs/prx/libSceFont/include/FontInternal.hpp b/core/libs/prx/libSceFont/include/FontInternal.hpp index ed6ba622f7..188151458c 100644 --- a/core/libs/prx/libSceFont/include/FontInternal.hpp +++ b/core/libs/prx/libSceFont/include/FontInternal.hpp @@ -134,6 +134,7 @@ int ComputeVerticalLayout(FontHandle handle, const StyleStateBlock* style, std:: int GetCharGlyphMetrics(FontHandle handle, std::uint32_t code, FontGlyphMetrics* metrics, bool useCachedStyle, float* usedScaleW = nullptr, float* usedScaleH = nullptr); int RenderCharGlyphImageCore(FontHandle handle, std::uint32_t code, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* result); int ComputeFontMetrics(FontHandle handle, std::uint32_t form, FontMetrics* metrics); +int ComputeFontStyleInformation(FontHandle handle, FontStyleInformation* information); } diff --git a/core/libs/prx/libSceFont/include/FontTypes.hpp b/core/libs/prx/libSceFont/include/FontTypes.hpp index 8af95a1276..1fab30eefc 100644 --- a/core/libs/prx/libSceFont/include/FontTypes.hpp +++ b/core/libs/prx/libSceFont/include/FontTypes.hpp @@ -442,6 +442,15 @@ struct FontMetrics { float slantRatio; }; +struct FontStyleInformation { + const char* familyName; + const char* styleName; + std::uint16_t styleFlags; + std::uint16_t weight; + std::uint16_t width; + std::uint16_t layoutFlags; +}; + static_assert(sizeof(FontHandleOpaque) == 0x100); static_assert(sizeof(FontMemory) == 0x40); static_assert(sizeof(FontMemoryInterface) == 0x30); @@ -453,6 +462,8 @@ static_assert(offsetof(FontMetrics, maxAdvance) == 0x10); static_assert(offsetof(FontMetrics, xMin) == 0x1C); static_assert(offsetof(FontMetrics, emWidth) == 0x2C); static_assert(offsetof(FontMetrics, effectWeightX) == 0x34); +static_assert(sizeof(FontStyleInformation) == 0x18); +static_assert(offsetof(FontStyleInformation, styleFlags) == 0x10); static_assert(sizeof(FontGlyphImageMetrics) == 0x18); static_assert(sizeof(FontGenerateGlyphDetail) == 0x20); static_assert(sizeof(FontResultStage) == 0x18); diff --git a/core/libs/prx/libSceFont/src/Layout.cpp b/core/libs/prx/libSceFont/src/Layout.cpp index 60151a4cae..edb6e02d58 100644 --- a/core/libs/prx/libSceFont/src/Layout.cpp +++ b/core/libs/prx/libSceFont/src/Layout.cpp @@ -567,3 +567,36 @@ int Font::ComputeFontMetrics(FontHandle handle, std::uint32_t form, FontMetrics* ReleaseFontCtxEntryLock(entry, modeLow, lockWord); return rc == SCE_FONT_OK ? rc : SCE_FONT_ERROR_INVALID_FONT_HANDLE; } + +int Font::ComputeFontStyleInformation(FontHandle handle, FontStyleInformation* information) { + auto* font = GetNativeFont(handle); + auto* library = static_cast(font->library); + if (!library) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + if (!information) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (const auto* record = static_cast(font->open_info.fontset_record)) { + std::memcpy(&information->familyName, record + 8, sizeof(information->familyName)); + std::memcpy(&information->styleName, record + 16, sizeof(information->styleName)); + information->styleFlags = record[4]; + information->weight = static_cast(record[5] * 100); + information->width = record[6]; + information->layoutFlags = record[7]; + return SCE_FONT_OK; + } + const SysDriver* driver = library->sys_driver; + auto* context = FontContext(library, font); + if (!driver || !driver->style_information || !context) return SCE_FONT_ERROR_FATAL; + const std::uint32_t modeLow = font->flags & 0x0Fu; + FontObj* head = nullptr; + std::uint32_t lockWord = 0; + auto* entry = AcquireFontCtxEntry(context, font->open_info.ctx_entry_index, modeLow, &head, &lockWord); + if (!entry) return SCE_FONT_ERROR_FATAL; + FontObj* match = FindSubFont(head, font->open_info.sub_font_index); + int rc = SCE_FONT_ERROR_FATAL; + if ((lockWord & OPEN_BIT) != 0 && (lockWord & COUNT_MASK) != 0 && match) { + match->font_handle = handle; + rc = driver->style_information(match, information); + information->weight = static_cast(information->weight * 100); + } + ReleaseFontCtxEntryLock(entry, modeLow, lockWord); + return rc; +} diff --git a/core/libs/prx/libSceFont/src/Render.cpp b/core/libs/prx/libSceFont/src/Render.cpp index 6d61ebb1b9..7e2871aeba 100644 --- a/core/libs/prx/libSceFont/src/Render.cpp +++ b/core/libs/prx/libSceFont/src/Render.cpp @@ -178,6 +178,26 @@ int APS5_VABI sceFontGetFontMetrics(FontHandle fontHandle, std::uint32_t form, F return SCE_FONT_ERROR_INVALID_FONT_HANDLE; } +int APS5_VABI sceFontGetFontStyleInformation(FontHandle fontHandle, FontStyleInformation* information) { + auto* font = GetNativeFont(fontHandle); + std::uint32_t fontLock = 0; + int rc = SCE_FONT_ERROR_INVALID_FONT_HANDLE; + if (font && font->magic == HANDLE_MAGIC && AcquireFontLock(font, fontLock)) { + rc = ComputeFontStyleInformation(fontHandle, information); + ReleaseFontLock(font, fontLock); + if (rc == SCE_FONT_OK) return rc; + } + if (information) { + information->familyName = ""; + information->styleName = ""; + information->styleFlags = 0; + information->weight = 0; + information->width = 0; + information->layoutFlags = 0; + } + return rc; +} + int APS5_VABI sceFontGetVerticalLayout(FontHandle fontHandle, FontVerticalLayout* layout) { int rc = SCE_FONT_ERROR_INVALID_FONT_HANDLE; auto* font = GetNativeFont(fontHandle); diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index 4e44d99f9a..5bb7e6fdd3 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -11,7 +11,6 @@ struct FontGraphicsServiceParam; struct FontWords; struct FontGlyphSupport; struct FontOutlineProfile; -struct FontStyleInformation; struct FontGlyphExpandBufferState; struct FontGraphicsResource; struct FontGraphicsFrame; @@ -183,11 +182,6 @@ int APS5_VABI sceFontGetFontGlyphsOutlineProfile(FontHandle font, FontOutlinePro return 0; } -int APS5_VABI sceFontGetFontStyleInformation(FontHandle font, FontStyleInformation* information) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontGetGlyphExpandBufferState(FontHandle font, FontGlyphExpandBufferState* state) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceFontFt/src/Driver.cpp b/core/libs/prx/libSceFontFt/src/Driver.cpp index 24fb3b07c6..a87e859fdd 100644 --- a/core/libs/prx/libSceFontFt/src/Driver.cpp +++ b/core/libs/prx/libSceFontFt/src/Driver.cpp @@ -881,6 +881,26 @@ int APS5_VABI LibraryGetFontMetrics(FontObj* obj, std::uint32_t form, FontMetric return SCE_FONT_OK; } +std::uint16_t WeightClass(std::uint16_t weight) { + return weight >= 100 && weight <= 900 && weight % 100 == 0 ? static_cast(weight / 100) : 0; +} + +int APS5_VABI LibraryGetFontStyleInformation(FontObj* obj, FontStyleInformation* information) { + const FT_Face face = obj ? FaceOf(obj) : nullptr; + if (!face || !information) return SCE_FONT_ERROR_FATAL; + const auto* header = static_cast(FT_Get_Sfnt_Table(face, FT_SFNT_HEAD)); + if (!header) throw std::runtime_error("FontFt: style information of a face without a head table"); + const auto* os2 = static_cast(FT_Get_Sfnt_Table(face, FT_SFNT_OS2)); + if (os2 && os2->version == 0xFFFF) os2 = nullptr; + information->familyName = face->family_name; + information->styleName = face->style_name; + information->styleFlags = static_cast(face->style_flags & (FT_STYLE_FLAG_ITALIC | FT_STYLE_FLAG_BOLD)); + information->weight = os2 ? WeightClass(os2->usWeightClass) : 0; + information->width = os2 && os2->usWidthClass < 10 ? os2->usWidthClass : 0; + information->layoutFlags = static_cast(1 + (header->Font_Direction < 0 ? 1 : 0) + (FT_HAS_VERTICAL(face) ? 0x80 : 0)); + return SCE_FONT_OK; +} + SysDriver MakeDriverTable() { SysDriver driver{}; driver.pixel_resolution = &LibraryGetPixelResolution; @@ -896,6 +916,7 @@ SysDriver MakeDriverTable() { driver.set_char_with_dpi = &LibrarySetCharSizeWithDpi; driver.set_char_default_dpi = &LibrarySetCharSizeDefaultDpi; driver.font_metrics = &LibraryGetFontMetrics; + driver.style_information = &LibraryGetFontStyleInformation; driver.compute_layout = &LibraryComputeLayout; driver.compute_layout_alt = &LibraryComputeLayoutAlt; driver.load_glyph_cached = &LibraryLoadGlyphCached; diff --git a/core/libs/tests/GuestFontStyleInformation.cpp b/core/libs/tests/GuestFontStyleInformation.cpp new file mode 100644 index 0000000000..b3e25044ba --- /dev/null +++ b/core/libs/tests/GuestFontStyleInformation.cpp @@ -0,0 +1,168 @@ +#include "prx/libSceFont/include/FontDriver.hpp" +#include "tests/FontFixture.hpp" +#include +#include +#include +#include +#include +#include +#include +#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 sceFontSupportExternalFonts(FontLibrary, std::uint32_t, std::uint32_t); +int APS5_VABI sceFontOpenFontMemory(FontLibrary, const void*, std::uint32_t, const FontOpenDetail*, FontHandle*); +int APS5_VABI sceFontCloseFont(FontHandle); +int APS5_VABI sceFontGetFontStyleInformation(FontHandle, FontStyleInformation*); +const void* APS5_VABI sceFontSelectLibraryFt(int); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Font style information check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +using FontFixture::BuildFont; +using FontFixture::Words; + +static void* APS5_VABI Allocate(void*, std::uint32_t size) { + return std::malloc(size); +} + +static void APS5_VABI Release(void*, void* pointer) { + std::free(pointer); +} + +static std::vector NameTable(std::string_view family, std::string_view style) { + const int familyBytes = static_cast(family.size() * 2); + const int styleBytes = static_cast(style.size() * 2); + std::vector table = Words({0, 2, 30, 3, 1, 0x409, 1, familyBytes, 0, 3, 1, 0x409, 2, styleBytes, familyBytes}); + for (const char c : family) FontFixture::Put16(table, c); + for (const char c : style) FontFixture::Put16(table, c); + return table; +} + +static std::vector Os2Table(int weight, int width) { + return Words({0, 500, weight, width, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0x20, 0x7E, 700, -300, 100, 900, 250}); +} + +static void PatchHead(std::vector& font, int macStyle, int direction) { + const int tableCount = (font[4] << 8) | font[5]; + for (int i = 0; i < tableCount; ++i) { + const std::size_t entry = 12 + 16 * static_cast(i); + if (std::memcmp(font.data() + entry, "head", 4) != 0) continue; + const std::size_t offset = (static_cast(font[entry + 8]) << 24) | (static_cast(font[entry + 9]) << 16) | + (static_cast(font[entry + 10]) << 8) | font[entry + 11]; + font[offset + 44] = static_cast((macStyle >> 8) & 0xFF); + font[offset + 45] = static_cast(macStyle & 0xFF); + font[offset + 48] = static_cast((direction >> 8) & 0xFF); + font[offset + 49] = static_cast(direction & 0xFF); + return; + } + Require(false); +} + +static FontHandle Open(FontLibrary library, const std::vector& data) { + FontHandle font = nullptr; + Require(sceFontOpenFontMemory(library, data.data(), static_cast(data.size()), nullptr, &font) == SCE_FONT_OK && font != nullptr); + return font; +} + +static FontStyleInformation Information(FontHandle font) { + FontStyleInformation information{}; + std::memset(&information, 0x7F, sizeof(information)); + Require(sceFontGetFontStyleInformation(font, &information) == SCE_FONT_OK); + return information; +} + +static bool Cleared(const FontStyleInformation& information) { + return information.familyName && information.familyName[0] == '\0' && information.styleName && information.styleName[0] == '\0' && + information.styleFlags == 0 && information.weight == 0 && information.width == 0 && information.layoutFlags == 0; +} + +int main() { + const FontMemoryInterface iface{Allocate, Release, nullptr, nullptr, nullptr, nullptr}; + FontMemory memory{}; + Require(sceFontMemoryInit(&memory, nullptr, 0, &iface, nullptr, nullptr, nullptr) == SCE_FONT_OK); + FontLibrary library = nullptr; + Require(sceFontCreateLibrary(&memory, sceFontSelectLibraryFt(0), &library) == SCE_FONT_OK); + Require(sceFontSupportExternalFonts(library, 4, 0x52) == SCE_FONT_OK); + + auto namedData = BuildFont(1, {{"name", NameTable("Tst", "Bd")}}); + PatchHead(namedData, 3, -2); + FontHandle named = Open(library, namedData); + const FontStyleInformation namedInfo = Information(named); + Require(namedInfo.familyName && std::string(namedInfo.familyName) == "Tst"); + Require(namedInfo.styleName && std::string(namedInfo.styleName) == "Bd"); + Require(namedInfo.styleFlags == 3 && namedInfo.weight == 0 && namedInfo.width == 0 && namedInfo.layoutFlags == 2); + + const auto os2Data = BuildFont(1, {{"name", NameTable("Tst", "Rg")}, {"OS/2", Os2Table(400, 5)}, + {"vhea", Words({0x11, 0, 880, -120, 60, 1000, 40, 0, 950, 0, 0, 0, 0, 0, 0, 0, 0, 1})}, {"vmtx", Words({1000, 40})}}); + FontHandle os2 = Open(library, os2Data); + const FontStyleInformation os2Info = Information(os2); + Require(os2Info.styleFlags == 0 && os2Info.weight == 400 && os2Info.width == 5 && os2Info.layoutFlags == 0x81); + + const auto oddData = BuildFont(1, {{"name", NameTable("Tst", "Rg")}, {"OS/2", Os2Table(450, 12)}}); + FontHandle odd = Open(library, oddData); + const FontStyleInformation oddInfo = Information(odd); + Require(oddInfo.weight == 0 && oddInfo.width == 0 && oddInfo.layoutFlags == 1); + + for (const int weight : {100, 900}) { + const auto data = BuildFont(1, {{"name", NameTable("Tst", "Rg")}, {"OS/2", Os2Table(weight, 9)}}); + FontHandle font = Open(library, data); + const FontStyleInformation info = Information(font); + Require(info.weight == weight && info.width == 9); + Require(sceFontCloseFont(font) == SCE_FONT_OK); + } + for (const int weight : {0, 1000}) { + const auto data = BuildFont(1, {{"name", NameTable("Tst", "Rg")}, {"OS/2", Os2Table(weight, 10)}}); + FontHandle font = Open(library, data); + const FontStyleInformation info = Information(font); + Require(info.weight == 0 && info.width == 0); + Require(sceFontCloseFont(font) == SCE_FONT_OK); + } + + static const char recordFamily[] = "Rf"; + static const char recordStyle[] = "Rs"; + std::array record{}; + record[4] = std::byte{1}; + record[5] = std::byte{7}; + record[6] = std::byte{3}; + record[7] = std::byte{0x81}; + const char* familyPointer = recordFamily; + const char* stylePointer = recordStyle; + std::memcpy(record.data() + 8, &familyPointer, sizeof(familyPointer)); + std::memcpy(record.data() + 16, &stylePointer, sizeof(stylePointer)); + auto* nativeFont = reinterpret_cast(os2); + nativeFont->open_info.fontset_record = record.data(); + const FontStyleInformation recordInfo = Information(os2); + nativeFont->open_info.fontset_record = nullptr; + Require(recordInfo.familyName == recordFamily && recordInfo.styleName == recordStyle); + Require(recordInfo.styleFlags == 1 && recordInfo.weight == 700 && recordInfo.width == 3 && recordInfo.layoutFlags == 0x81); + + FontStyleInformation cleared{}; + std::memset(&cleared, 0x7F, sizeof(cleared)); + Require(sceFontGetFontStyleInformation(nullptr, &cleared) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && Cleared(cleared)); + std::uint64_t notAFont[32]{}; + std::memset(&cleared, 0x7F, sizeof(cleared)); + Require(sceFontGetFontStyleInformation(reinterpret_cast(notAFont), &cleared) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && Cleared(cleared)); + Require(sceFontGetFontStyleInformation(named, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontGetFontStyleInformation(nullptr, nullptr) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + + for (FontHandle font : {named, os2, odd}) Require(sceFontCloseFont(font) == SCE_FONT_OK); + Require(sceFontDestroyLibrary(&library) == SCE_FONT_OK); + Require(sceFontMemoryTerm(&memory) == SCE_FONT_OK); + std::puts("Font style information tests passed"); + return 0; +} diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index c59aa9fb2b..0384b89ce5 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -23,9 +23,10 @@ - [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - the console file names are those of the PS4 firmware; the names of the PS5-only JG2 sets (`0x1A......`) are unknown, so they use the SSTJpPro files. When a console file is missing, an openly licensed substitute (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans Arabic, Noto Sans CJK with its Korean face for Hangul) is loaded instead; its metrics differ, so text width, line height and wrapping differ from the console. System font faces are loaded by FreeType in host memory, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. - [sceFontGetCharGlyphMetrics, sceFontGetRenderCharGlyphMetrics](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) report the vertical metrics the way the [FreeType driver](../../../core/libs/prx/libSceFontFt/src/Driver.cpp) reports them for a rendered glyph, not confirmed on a title: FreeType's vertical advance, and for a face without vertical metrics a bearing of half the horizontal advance left of the horizontal bearing and of half the advance left over by the glyph height. - [sceFontGetFontMetrics](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) - the 64-byte output, the form fallbacks (2 and 3 fall back to the `hhea` values when OS/2 is missing or gives an empty range, 4 builds vertical values from `vhea` or OS/2) and the raw weight deltas and slant copied from the style are read from the disassembly of firmware 13.60 `libSceFont.sprx` and `libSceFontFt.sprx`, not measured on a console. For a font set the values are those of its primary file; which file the console reports is not recovered. Closes when: measured on a PS5 +- [sceFontGetFontStyleInformation](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) - the 24-byte output (FreeType family and style names, italic and bold flags, OS/2 weight class when it is a multiple of 100 from 100 to 900, width class below 10, and 1 plus a right-to-left `head` direction plus 0x80 for vertical metrics) and the font-set record path are read from the disassembly of firmware 13.60 `libSceFont.sprx` and `libSceFontFt.sprx`, not measured on a console. A face without a `head` table, such as a BDF font, throws: the console reads the table without checking it. Closes when: measured on a PS5 - [Font writing lines](../../../core/libs/prx/libSceFont/src/WritingLine.cpp): visual ordering and spacing originated from PS4 measurements and match the supplied PS5 structure and control flow. Exhaustive floating-point equivalence, including NaN payloads and rounding, has not been verified against a running PS5 module. -- [Font graphics and metric signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): graphics-device/service creation takes font memory, parameters and a handle output; destruction takes a handle pointer. Attribute selectors and outputs are 32-bit values. Outline-profile, style-information and expansion-buffer outputs remain opaque pending field recovery. Protected implementations are unchanged. +- [Font graphics and metric signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): graphics-device/service creation takes font memory, parameters and a handle output; destruction takes a handle pointer. Attribute selectors and outputs are 32-bit values. Outline-profile and expansion-buffer outputs remain opaque pending field recovery. Protected implementations are unchanged. - [Font graphics fill and transform signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): region layout and scalar float arguments are confirmed from assembly, including the extra 32-bit corner flags omitted by the roundish-region decompilation. Fill-method, plot and rates layouts, layout selector meanings and mapping parameter meanings remain unresolved. Protected bodies are unchanged. From db9e8a846bead28bafb6574ba8d147b1b0cd62f9 Mon Sep 17 00:00:00 2001 From: orezraey Date: Sat, 24 Oct 2026 02:33:20 -0300 Subject: [PATCH 182/428] fix(libkernel): ignore the sendto address of a connected stream socket Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 9 ++++-- core/libs/tests/GuestSocketAddressLength.cpp | 31 +++++++++++++++----- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index d06ffbb272..36070cf7ea 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -317,6 +317,11 @@ bool Listening(const Socket& socket) { bool listening = false; return GuestSockets::NativeListening(static_cast(socket.value), listening) && listening; } +bool PeerConnected(const Socket& socket) { + sockaddr_storage peer{}; + socklen_t size = sizeof(peer); + return ::getpeername(socket.value, reinterpret_cast(&peer), &size) == 0; +} constexpr std::int64_t NotAnswered = -2; std::int64_t ZeroPortSend(const Socket& socket, const sockaddr_storage& destination, std::uint64_t length) { if (socket.type != 2 || socket.family != 2 || destination.ss_family != AF_INET) return NotAnswered; @@ -1420,8 +1425,8 @@ std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, st if (!buffer && length) return Fail(14); if (const int waited = WaitForTransfer(*socket, true, flags)) return Fail(waited); int result; - if (!address) { - if (addressLength != 0) return Fail(22); + if (!address || (socket->type == 1 && PeerConnected(*socket))) { + if (!address && addressLength != 0) return Fail(22); result = ::send(socket->value, static_cast(buffer), static_cast(length), SocketSendFlags(*socket, flags)); } else { sockaddr_storage native{}; diff --git a/core/libs/tests/GuestSocketAddressLength.cpp b/core/libs/tests/GuestSocketAddressLength.cpp index 7fd0d783b9..0342439bd1 100644 --- a/core/libs/tests/GuestSocketAddressLength.cpp +++ b/core/libs/tests/GuestSocketAddressLength.cpp @@ -74,10 +74,10 @@ void ArgumentsAreCheckedBeforeTheDescriptor() { void LongAddressesAndOtherFamiliesFail() { constexpr int ErrorFamily = 47; - const int stream = socket_nid_postfix(2, Stream, 0); const int datagram = socket_nid_postfix(2, Datagram, 0); const int stream6 = socket_nid_postfix(28, Stream, 0); - Require(stream >= 0 && datagram >= 0 && stream6 >= 0, "socket failed"); + const int datagram6 = socket_nid_postfix(28, Datagram, 0); + Require(datagram >= 0 && stream6 >= 0 && datagram6 >= 0, "socket failed"); std::array address{16, 2}; address[4] = 127; address[7] = 1; @@ -92,16 +92,32 @@ void LongAddressesAndOtherFamiliesFail() { Require(Fails(connect_nid_postfix(stream6, v6.data(), 64), ErrorInvalid), "connect with an AF_INET6 address longer than sockaddr_in6 must fail with EINVAL (22)"); for (const std::uint8_t family : {std::uint8_t{28}, std::uint8_t{1}, std::uint8_t{0}}) { address[1] = family; - Require(Fails(bind_nid_postfix(stream, address.data(), 16), ErrorFamily), "bind of another family on an AF_INET socket must fail with EAFNOSUPPORT (47)"); + Require(Fails(bind_nid_postfix(datagram, address.data(), 16), ErrorFamily), "bind of another family on an AF_INET socket must fail with EAFNOSUPPORT (47)"); } address[1] = 28; - Require(Fails(bind_nid_postfix(stream, address.data(), 28), ErrorFamily), "the family is checked before the length on an AF_INET socket"); + Require(Fails(bind_nid_postfix(datagram, address.data(), 28), ErrorFamily), "the family is checked before the length on an AF_INET socket"); Require(Fails(sendto_nid_postfix(datagram, "x", 1, 0, address.data(), 28), ErrorInvalid), "sendto checks the length before the family"); Require(Fails(sendto_nid_postfix(datagram, "x", 1, 0, address.data(), 16), ErrorFamily), "sendto of another family fails with EAFNOSUPPORT (47)"); address[1] = 2; - Require(Fails(bind_nid_postfix(stream6, address.data(), 16), ErrorInvalid), "an AF_INET6 socket checks the length before the family"); - Require(Fails(connect_nid_postfix(stream6, address.data(), 28), ErrorFamily), "an AF_INET address of the AF_INET6 length fails with EAFNOSUPPORT (47)"); - Require(close_nid_postfix(stream) == 0 && close_nid_postfix(datagram) == 0 && close_nid_postfix(stream6) == 0, "close failed"); + Require(Fails(bind_nid_postfix(datagram6, address.data(), 16), ErrorInvalid), "an AF_INET6 socket checks the length before the family"); + Require(Fails(connect_nid_postfix(datagram6, address.data(), 28), ErrorFamily), "an AF_INET address of the AF_INET6 length fails with EAFNOSUPPORT (47)"); + Require(close_nid_postfix(datagram) == 0 && close_nid_postfix(stream6) == 0 && close_nid_postfix(datagram6) == 0, "close failed"); +} + +void ConnectedStreamSendtoIgnoresTheAddress() { + const int listener = socket_nid_postfix(2, Stream, 0); + const int client = socket_nid_postfix(2, Stream, 0); + Require(listener >= 0 && client >= 0, "socket failed"); + auto address = Loopback(); + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0 && listen_nid_postfix(listener, 1) == 0, "listen failed"); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &size) == 0, "getsockname failed"); + Require(connect_nid_postfix(client, address.data(), address.size()) == 0, "connect failed"); + std::array padded{}; + std::memcpy(padded.data(), address.data(), address.size()); + Require(sendto_nid_postfix(client, "x", 1, 0, padded.data(), padded.size()) == 1, "tcp_usr_send ignores the address of a connected stream socket, so sendto must send"); + Require(Fails(sendto_nid_postfix(client, "x", 1, 0, padded.data(), 1), ErrorInvalid), "getsockaddr still checks the length of a connected stream socket's address"); + Require(close_nid_postfix(client) == 0 && close_nid_postfix(listener) == 0, "close failed"); } void FullAddressesStillWork() { @@ -176,6 +192,7 @@ int main() { ShortAddressesAreInvalid(); ArgumentsAreCheckedBeforeTheDescriptor(); LongAddressesAndOtherFamiliesFail(); + ConnectedStreamSendtoIgnoresTheAddress(); FullAddressesStillWork(); LongAddressesAreInvalid(2, Stream); LongAddressesAreInvalid(2, Datagram); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 891ab90b14..8c1cade16f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -206,7 +206,7 @@ - [MSG_WAITALL](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) in `recv`, `recvfrom` and `recvmsg` on a stream socket is passed to the host, which returns when the buffer is full, the peer closes or an error occurs, as FreeBSD 11.4's [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1855-L1860) does; a datagram socket ignores it. It is not measured on a console, a nonblocking socket and a call that also passes `MSG_PEEK` follow the host's rules, and `SO_RCVTIMEO` bounds only the wait for the first data. Any other flag still fails with `EOPNOTSUPP`. - [recv and recvfrom](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answer without the host a zero-length receive on a stream socket (0, or `ENOTCONN` when it is not connected, [L1566-L1578](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1566-L1578)) and, on Windows only, a receive on a socket whose read side `shutdown` closed (0, as [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1556-L1562) returns at `SBS_CANTRCVMORE`); Winsock fails the second with `EPIPE` and blocks on the first. Linux and macOS still return data buffered before the `shutdown`, where FreeBSD discards it. `recvmsg` still asks the host in both cases. Not measured on a console. - [send, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - send and sendto without an address on an unconnected datagram socket map Windows `WSAENOTCONN` to FreeBSD's `EDESTADDRREQ` (39). -- [bind, connect, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) check a socket address as a payload process on a PS5 (firmware 13.60) did: a length below 2 fails with `EINVAL` and above 255 with `ENAMETOOLONG`; an `AF_INET` socket needs exactly 16 bytes and an `AF_INET6` socket 28, or `EINVAL`, and another family fails with `EAFNOSUPPORT`. On an `AF_INET` socket `bind` and `connect` check the family first and `sendto` the length first; an `AF_INET6` socket checks the length first for all three. `connect` on a listening stream socket fails with `EOPNOTSUPP` before these checks, as FreeBSD 11.4's `soconnect` does; that order was not measured +- [bind, connect, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) check a socket address as a payload process on a PS5 (firmware 13.60) did: a length below 2 fails with `EINVAL` and above 255 with `ENAMETOOLONG`; an `AF_INET` socket needs exactly 16 bytes and an `AF_INET6` socket 28, or `EINVAL`, and another family fails with `EAFNOSUPPORT`; `AF_UNSPEC` with `INADDR_ANY`, which FreeBSD's `bind` accepts, is unmeasured and fails too. On an `AF_INET` socket `bind` and `connect` check the family first and `sendto` the length first; an `AF_INET6` socket checks the length first for all three. Only datagram sockets were measured; stream sockets follow the same order. A connected stream socket's `sendto` ignores the address once its length passes, as `tcp_usr_send` does. `connect` on a listening stream socket fails with `EOPNOTSUPP` before these checks, as FreeBSD 11.4's `soconnect` does; that order was not measured - [bind, connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) on an `AF_INET6` datagram socket with `IPV6_V6ONLY` cleared accept an IPv4-mapped address longer than 28 bytes and use its first 28, as FreeBSD 11.4's `udp6_bind` and `udp6_connect` hand it to `in_pcbbind` and `in_pcbconnect` as a `sockaddr_in` without the length test of `in6_pcbbind`; `sendto` still fails it with `EINVAL`, as `udp6_send` tests the length first. The console was measured only with `::1` and `IPV6_V6ONLY` at its default, so this case is not measured - [read, write and vector I/O](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. `EPIPE` from a write raises the host's `SIGPIPE` on Linux unless the socket has `SO_NOSIGPIPE` set, and the guest cannot ignore it: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; nothing is raised on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` From 0e1767b24b8cf51d13a318f0b442159fb8b22e8e Mon Sep 17 00:00:00 2001 From: milescoviello Date: Fri, 23 Oct 2026 22:40:35 +0000 Subject: [PATCH 183/428] fix(shader): release specialized modules that no result uses The specialized module caches (by variant and constants, by binding plan and by registration key) and the SPIR-V interning table held every module's words for the rest of the run, after the results that used them were gone. They now keep a module's interface (bindings, push data, specialization ID and content hash) and only a weak reference to its words; a lookup after the words were released builds them again from the same key, checks the content hash, and keeps the specialization ID that names the module's pipelines. The interning table keeps weak references and drops expired ones from the bucket it searches. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 31 +++++++ .../prx/libSceAgcDriver/tests/SpirvModule.cpp | 15 ++++ core/shader/recompiler/Recompiler.cpp | 82 +++++++++++++++---- core/shader/recompiler/Recompiler.hpp | 21 +++++ 4 files changed, 132 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index f015fad6b4..69d35f1282 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -406,6 +406,36 @@ void verifyFrontendPair() { } } +void verifySpecializedModuleRelease() { + using namespace ShaderRecompiler; + const std::array color{0x3f800000u}; + const std::vector code{0xbf800003u, 0x7e000200u, 0xf800180fu, 0u, 0xbf810000u}; + auto pixel = FrontendRequest(code, color); + pixel.shader.stage = ShaderStage::Fragment; + pixel.context.compute.reset(); + pixel.context.pixel = ShaderPixelStageInfo{}; + pixel.context.pixel->wave32 = true; + pixel.context.pixel->targetOutputMode[0] = 9u; + pixel.context.waveSize = 32u; + pixel.useCache = true; + SharedSpirv::Weak released; + std::uint64_t specializationId = 0; + std::vector words; + { + const auto first = Recompile(pixel); + require(first.specializationId != 0u, "specialized module release: the pixel fixture was not specialized"); + released = first.spirv.Weaken(); + specializationId = first.specializationId; + words = first.spirv.Copy(); + } + require(released.Expired(), "specialized module release: the module caches kept the words of a module no result uses"); + const auto again = Recompile(pixel); + require(again.specializationId == specializationId, "specialized module release: a module built again from its key got another specialization ID"); + require(std::ranges::equal(again.spirv.Words(), words), "specialized module release: a module built again from its key has other words"); + const auto shared = Recompile(pixel); + require(shared.spirv.data() == again.spirv.data(), "specialized module release: a second result of a live module copied its words"); +} + ShaderRecompiler::RecompileRequest LayoutPairRequest(std::span code, std::span userData, bool registration, std::uint32_t wordLimit) { using namespace ShaderRecompiler; auto request = FrontendRequest(code, userData); @@ -8466,6 +8496,7 @@ int main(int argc, char** argv) { verifyTargetFeatureKey(); verifyImageTableKeys(); verifyImageTables(); + verifySpecializedModuleRelease(); verifyImageTableRegistration(); verifyScalarAddressRule(); verifyPointerImageTable(); diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp index 05d848cdf2..956897cb51 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp @@ -460,6 +460,20 @@ void testInternedSpirvSharesEqualWords() { check(edited[6] == spv::CapabilityFloat64 && first[6] == spv::CapabilityShader && again.data() == first.data(), "a write to one holder of interned SPIR-V changed the others"); } +void testInternedSpirvReleasesUnusedWords() { + const std::vector words{spv::MagicNumber, 0x00010400u, 0u, 7u, 0u, (2u << 16u) | spv::OpCapability, spv::CapabilityGeometry}; + SharedSpirv::Weak released; + { + const auto first = InternSpirv(words); + const auto second = InternSpirv(words); + check(first.data() == second.data(), "two equal specialized modules kept separate copies of their words"); + released = first.Weaken(); + } + check(released.Expired(), "the SPIR-V interning table kept words no holder uses"); + const auto again = InternSpirv(words); + check(std::ranges::equal(again.Words(), words) && released.Lock().empty(), "SPIR-V interned again after its release changed its words"); +} + int main() { testFoldingLeavesNoDeadBlocksOrConstants(); testIdsBeyondTheBoundAreRejected(); @@ -477,6 +491,7 @@ int main() { testEmittedWordsAreStable(); testBlockBallotsOfOnePredicateShareOneBallot(); testInternedSpirvSharesEqualWords(); + testInternedSpirvReleasesUnusedWords(); if (failures != 0) { std::fprintf(stderr, "%d check(s) failed\n", failures); return 1; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 8c90d56488..f959e013c1 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -746,12 +746,19 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra struct SpecializedModule { std::uint64_t specializationId = 0; - SharedSpirv spirv; std::vector bindings; bool pushData = false; + SpirvContentHash hash; + mutable std::mutex mutex; + mutable SharedSpirv::Weak spirv; }; -std::shared_ptr buildSpecializedModule(const CompiledShaderArtifact& artifact, std::span classes, std::span constants, const SpirvTarget& target) { +struct SpecializedSpirv { + std::shared_ptr module; + SharedSpirv spirv; +}; + +std::vector specializeWords(const CompiledShaderArtifact& artifact, std::span classes, std::span constants, const SpirvTarget& target) { auto source = SpecializeVertexInputTypes(artifact, classes); std::map supplied; for (const auto& constant : constants) { @@ -845,6 +852,11 @@ std::shared_ptr buildSpecializedModule(const CompiledSh #if ANYPS5_ENABLE_SPIRV_TOOLS materialized = ValidateAndOptimizeSpirv(materialized, target.vulkanVersion, target.spirvVersion, target.nonConstantImageOffsets, false); #endif + return materialized; +} + +SpecializedSpirv buildSpecializedModule(const CompiledShaderArtifact& artifact, std::span classes, std::span constants, const SpirvTarget& target) { + auto materialized = specializeWords(artifact, classes, constants, target); auto module = std::make_shared(); std::map bindingNumbers; std::unordered_set functionLdsBuffer; @@ -859,9 +871,20 @@ std::shared_ptr buildSpecializedModule(const CompiledSh } cursor += count; } - module->spirv = InternSpirv(std::move(materialized)); + auto spirv = InternSpirv(std::move(materialized)); + module->hash = spirv.ContentHash(); + module->spirv = spirv.Weaken(); module->specializationId = constants.empty() ? 0u : nextVariantId(); - return module; + return {std::move(module), std::move(spirv)}; +} + +SharedSpirv specializedWords(const SpecializedModule& module, const CompiledShaderArtifact& artifact, std::span classes, std::span constants, const SpirvTarget& target) { + std::lock_guard lock(module.mutex); + if (auto spirv = module.spirv.Lock(); !spirv.empty()) return spirv; + auto spirv = InternSpirv(specializeWords(artifact, classes, constants, target)); + if (spirv.ContentHash() != module.hash) throw std::runtime_error("ShaderRecompiler: a specialized module built again from its key differs from its first build"); + module.spirv = spirv.Weaken(); + return spirv; } struct SpecializedModuleEntry { @@ -891,7 +914,12 @@ struct RegistrationModuleEntry { std::shared_ptr module; }; -std::shared_ptr registrationModule(std::uint64_t variantId, std::span moduleKey, const std::function()>& specialize) { +SharedSpirv lockedWords(const SpecializedModule& module) { + std::lock_guard lock(module.mutex); + return module.spirv.Lock(); +} + +SpecializedSpirv registrationModule(std::uint64_t variantId, std::span moduleKey, const std::function& specialize) { std::vector key{variantId}; key.insert(key.end(), moduleKey.begin(), moduleKey.end()); static PrimedSharedMutex mutex; @@ -906,15 +934,20 @@ std::shared_ptr registrationModule(std::uint64_t varian const auto found = modules.find(key); entry = found != modules.end() ? found->second : modules.emplace(key, std::make_shared()).first->second; } + SharedSpirv built; std::call_once(entry->ready, [&] { try { - entry->module = specialize(); + auto specialized = specialize(); + entry->module = std::move(specialized.module); + built = std::move(specialized.spirv); } catch (...) { entry->failure = std::current_exception(); } }); if (entry->failure) std::rethrow_exception(entry->failure); - return entry->module; + if (built.empty()) built = lockedWords(*entry->module); + if (built.empty()) built = specialize().spirv; + return {entry->module, std::move(built)}; } std::shared_ptr preparedBindingPlan(const CompiledVariant& variant, const ResourceSnapshot& snapshot, std::span exports, bool fragmentDescriptorSet) { @@ -967,7 +1000,7 @@ std::shared_ptr preparedBindingPlan(const CompiledVariant& return plan; } -std::shared_ptr specializeModule(const CompiledShaderArtifact& artifact, std::span classes, std::span constants, const SpirvTarget& target) { +SpecializedSpirv specializeModule(const CompiledShaderArtifact& artifact, std::span classes, std::span constants, const SpirvTarget& target) { struct ModuleKeyStorage {}; auto& key = HostThreadLocal, ModuleKeyStorage>(); key.clear(); @@ -985,15 +1018,19 @@ std::shared_ptr specializeModule(const CompiledShaderAr const auto found = modules.find(key); entry = found != modules.end() ? found->second : modules.emplace(key, std::make_shared()).first->second; } + SharedSpirv built; std::call_once(entry->ready, [&] { try { - entry->module = buildSpecializedModule(artifact, classes, constants, target); + auto specialized = buildSpecializedModule(artifact, classes, constants, target); + entry->module = std::move(specialized.module); + built = std::move(specialized.spirv); } catch (...) { entry->failure = std::current_exception(); } }); if (entry->failure) std::rethrow_exception(entry->failure); - return entry->module; + if (built.empty()) built = specializedWords(*entry->module, artifact, classes, constants, target); + return {entry->module, std::move(built)}; } const ResourceSnapshot& mergeTargetImage(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& snapshot, bool registrationDefaults, ResourceSnapshot& merged) { @@ -1115,7 +1152,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil const auto found = plan->modules.find(moduleKey); entry = found != plan->modules.end() ? found->second : plan->modules.emplace(moduleKey, std::make_shared()).first->second; } - const auto prepare = [&] { + const auto specialize = [&] { auto constants = plan->bindings.specialization; std::size_t index = 0; if (variant.bindings.layout.UsesPushData()) constants.push_back({PipelineSpecialization::PushDataOffset, moduleKey[index++]}); @@ -1140,11 +1177,17 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil constants.push_back({first + 5u, kind}); } } - const auto module = artifact.registrationModule && plan->bindings.specialization.empty() ? registrationModule(artifact.variantId, moduleKey, [&] { return specializeModule(variant.artifact, vertexClasses, constants, request.target); }) : specializeModule(variant.artifact, vertexClasses, constants, request.target); + return artifact.registrationModule && plan->bindings.specialization.empty() ? registrationModule(artifact.variantId, moduleKey, [&] { return specializeModule(variant.artifact, vertexClasses, constants, request.target); }) : specializeModule(variant.artifact, vertexClasses, constants, request.target); + }; + SharedSpirv built; + const auto prepare = [&] { + auto specialized = specialize(); + const auto& module = specialized.module; if (variant.bindings.layout.UsesPushData() && !module->pushData) throw std::runtime_error("specialization removed the prepared push constant interface"); auto selected = DescriptorBindingBuilder{}.Select(plan->bindings, module->bindings); entry->bindings = std::move(selected); entry->module = module; + built = std::move(specialized.spirv); }; std::call_once(entry->ready, [&] { try { @@ -1155,8 +1198,10 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil }); if (entry->failure) std::rethrow_exception(entry->failure); const auto& module = entry->module; + if (built.empty()) built = lockedWords(*module); + if (built.empty()) built = specialize().spirv; result.specializationId = module->specializationId; - result.spirv = module->spirv; + result.spirv = std::move(built); bindingPlan = &entry->bindings; } else { result.spirv = artifact.spirv; @@ -1214,13 +1259,16 @@ SharedSpirv InternSpirv(std::vector words) { SharedSpirv spirv(std::move(words)); const auto hash = spirv.ContentHash(); static std::mutex mutex; - static std::unordered_map> interned; + static std::unordered_map> interned; std::lock_guard lock(mutex); - auto& candidates = interned[hash.low ^ (hash.high * 0x9e3779b97f4a7c15ull)]; - for (const auto& candidate : candidates) { + const auto bucket = hash.low ^ (hash.high * 0x9e3779b97f4a7c15ull); + auto& candidates = interned[bucket]; + std::erase_if(candidates, [](const SharedSpirv::Weak& candidate) { return candidate.Expired(); }); + for (const auto& weak : candidates) { + const auto candidate = weak.Lock(); if (candidate.ContentHash() == hash && candidate == spirv) return candidate; } - candidates.push_back(spirv); + candidates.push_back(spirv.Weaken()); return spirv; } diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 9462ba0721..212e6b2f8d 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -603,6 +603,8 @@ inline std::uint64_t HashSpirvWords(std::span words, std::u // reference, or the per-dispatch copy this class removes comes back without a compiler hint. // ContentHash is computed once per shared holder, on first use, and dropped by a write. class SharedSpirv { + struct Holder; + public: SharedSpirv() = default; SharedSpirv(std::vector words) : holder(std::make_shared(std::move(words), nullptr, std::span{})) {} @@ -639,6 +641,25 @@ public: std::vector::iterator insert(std::vector::const_iterator where, std::initializer_list values) { return Mutable().insert(where, values); } friend bool operator==(const SharedSpirv& left, const SharedSpirv& right) { return std::ranges::equal(left.Words(), right.Words()); } + class Weak { + public: + [[nodiscard]] SharedSpirv Lock() const { + SharedSpirv spirv; + spirv.holder = holder.lock(); + return spirv; + } + [[nodiscard]] bool Expired() const { return holder.expired(); } + + private: + friend class SharedSpirv; + std::weak_ptr holder; + }; + [[nodiscard]] Weak Weaken() const { + Weak weak; + weak.holder = holder; + return weak; + } + private: struct Holder { Holder(std::vector words, std::shared_ptr owner, std::span view) : words(std::move(words)), owner(std::move(owner)), view(view) {} From 611daf97e825ef0e4c3a462667ce394062205365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 24 Oct 2026 10:22:12 +0200 Subject: [PATCH 184/428] test(shader): keep the recompile result alive while its words are read Co-Authored-By: Claude Sonnet 5.5 --- .../prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp | 3 ++- .../prx/libSceAgcDriver/tests/RedundantValueElimination.cpp | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp b/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp index cc952f5ea9..5dd5cfc4ba 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp @@ -110,7 +110,8 @@ std::vector Recompiled(std::span code) { request.target.maxWorkgroupSharedMemoryBytes = 49152; request.layout = {0, 0, 0, 128}; request.useCache = false; - const auto& words = Recompile(request).spirv.Words(); + const auto result = Recompile(request); + const auto& words = result.spirv.Words(); return {words.begin(), words.end()}; } diff --git a/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp b/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp index cc88c42cfe..2aec9adc4f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp @@ -208,7 +208,8 @@ std::vector Recompiled(std::span code) { request.target.maxWorkgroupSharedMemoryBytes = 49152; request.layout = {0, 0, 0, 128}; request.useCache = false; - const auto& words = Recompile(request).spirv.Words(); + const auto result = Recompile(request); + const auto& words = result.spirv.Words(); return {words.begin(), words.end()}; } From d36311d6614acecb2dafa74b3501ae7b79b93625 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Sat, 24 Oct 2026 15:45:26 +0700 Subject: [PATCH 185/428] fix(agc): keep a drawn linear target's draws per 64 KiB block over a direct write's completion stamp A direct write with completion work, recorded before draws into a resident linear target, stamps the target's memory again when its batch completes. The keep rule moved the whole image past that stamp only when nothing in the image had changed since its generation, so a CPU store anywhere else in the target, made before the completion, dropped every draw in the direct write's range at the next store. For a drawn target, when the whole-image check fails, the stamped 64 KiB blocks are now checked one by one. A block with no change since the image's generation is recorded as kept at the stamp (completionKept), and writeBackLayers and the unwatch path read that per-block generation, so the draws there are stored while a block the CPU wrote keeps the CPU's bytes outside the drawn rectangles. Whole-image keeps of drawn targets are rechecked for CPU stores after the stamp and fall back to the per-block keep. The per-block values are cleared when the layer starts a new pending period. agc_driver_linear_target_direct_write_completion gains the ordering from review: a fill over block 0, a draw into block 0, a CPU store into block 1 while the fill's completion is still pending, then the label, on write-watched and unwatched memory. Co-Authored-By: Claude --- .../Graphics/include/Texture.hpp | 2 + .../libSceAgcDriver/Graphics/src/Texture.cpp | 104 +++++++++++--- .../LinearTargetDirectWriteCompletion.cpp | 133 +++++++++++++++--- docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 207 insertions(+), 34 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 42b1b7940c..85a45ae60e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -543,6 +543,7 @@ private: // Per 64 KiB tracker block of the surface (block 0 holds the base address): the generation of // the tracked layer it belongs to, for GuestMemory::ChangedBlocks. void blockGenerations(std::vector& generations) const; + std::uint64_t unitBlockGeneration(std::uint32_t unit, std::uint64_t at) const; // Another image of exactly this surface under another storage format with results pending // (block units, uncompressed, still cached), if any: Refresh takes its pending units from its // image on the device (borrowUnits) instead of having them stored and re-read, and the alias @@ -627,6 +628,7 @@ private: std::uint32_t trackedLayers = 1; std::uint64_t trackedLayerBytes = 0; std::vector layerGeneration; + std::vector completionKept; std::vector layerPending; std::vector layerBehind; bool blockUnits = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index e8902dc7cd..aec8364a92 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3840,7 +3840,13 @@ void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) } } if (anyUnsaved && !GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) captureGuestBytes(&unsaved); - for (std::uint32_t layer = first; layer < first + count; ++layer) layerPending[layer] = true; + for (std::uint32_t layer = first; layer < first + count; ++layer) { + if (!layerPending[layer] && !completionKept.empty()) { + const auto end = layerBegin(layer) + layerBytes(layer); + for (auto at = layerBegin(layer) & ~std::uint64_t{65535}; at < end; at += 65536) completionKept[static_cast(at / 65536 - descriptor.baseAddress / 65536)] = 0; + } + layerPending[layer] = true; + } // Results of this image now cover the alias's results borrowed into these units: this image // stores them, the alias no longer has to (its results, unchanged since the borrow, are in // this image's content). An alias written since keeps its own newer results pending. @@ -4354,7 +4360,8 @@ void StorageTexture::saveBeforeUnwatch() { const auto end = begin + layerBytes(unit); const auto spanBegin = begin & ~(block - 1); const auto spanBlocks = static_cast((end - spanBegin + block - 1) / block); - const std::vector generations(spanBlocks, layerGeneration[unit]); + std::vector generations(spanBlocks); + for (std::size_t k = 0; k < spanBlocks; ++k) generations[k] = unitBlockGeneration(unit, spanBegin + k * block); std::vector changedBlocks(spanBlocks); if (!GuestMemory::ChangedBlocks(begin, static_cast(end - begin), generations, changedBlocks)) continue; forgetKeptAtUnwatch(unit); @@ -4362,7 +4369,7 @@ void StorageTexture::saveBeforeUnwatch() { const auto from = std::max(at, begin); const auto to = std::min(at + block, end); const bool edge = from != at || to != at + block; - if (changedBlocks[static_cast((at - spanBegin) / block)] == GuestMemory::BlockWritten && (!edge || GuestMemory::StoredOver(from, static_cast(to - from), layerGeneration[unit])) && !unchangedSinceBaseline(from, to)) keptAtUnwatch.emplace(from, layerGeneration[unit]); + if (changedBlocks[static_cast((at - spanBegin) / block)] == GuestMemory::BlockWritten && (!edge || GuestMemory::StoredOver(from, static_cast(to - from), unitBlockGeneration(unit, at))) && !unchangedSinceBaseline(from, to)) keptAtUnwatch.emplace(from, layerGeneration[unit]); } layerOriginal[unit] = true; cancelStoredSave(unit); @@ -4635,7 +4642,21 @@ bool PendingStorageOverlaps(std::uint64_t address, std::size_t bytes, const Stor } std::uint64_t StorageTexture::StampKeepingNewerResults(std::uint64_t address, std::size_t bytes, std::uint64_t recorded, const std::function& stamp) { - std::vector, std::uint32_t>> kept; + struct Candidate { + std::shared_ptr texture; + std::uint32_t unit; + bool drawn; + }; + struct Kept { + std::shared_ptr texture; + std::uint32_t unit; + std::uint64_t from; + std::uint64_t to; + std::uint64_t since; + bool whole; + bool drawn; + }; + std::vector candidates; if (recorded != 0 && bytes != 0) { auto& pending = Pending(); std::lock_guard lock(pending.mutex); @@ -4647,27 +4668,70 @@ std::uint64_t StorageTexture::StampKeepingNewerResults(std::uint64_t address, st const auto begin = texture->layerBegin(unit); const auto unitBytes = texture->layerBytes(unit); if (!texture->layerPending[unit] || begin >= address + bytes || address >= begin + unitBytes) continue; - if (texture->layerGeneration[unit] >= recorded) kept.emplace_back(alive, unit); + if (texture->layerGeneration[unit] >= recorded) candidates.push_back({alive, unit, texture->drawnListed}); } }; for (auto* texture : pending.textures) consider(texture); for (auto* texture : pending.flushing) consider(texture); } - std::erase_if(kept, [](const auto& entry) { - const auto& [texture, unit] = entry; + constexpr std::uint64_t block = 65536; + std::vector kept; + for (const auto& [texture, unit, drawn] : candidates) { const auto begin = texture->layerBegin(unit); - const auto unitBytes = static_cast(texture->layerBytes(unit)); - GuestMemory::CollectWritesUncached(begin, unitBytes); - return !GuestMemory::UnchangedSince(begin, unitBytes, texture->layerGeneration[unit]); - }); + const auto end = begin + texture->layerBytes(unit); + GuestMemory::CollectWritesUncached(begin, static_cast(end - begin)); + if (GuestMemory::UnchangedSince(begin, static_cast(end - begin), texture->layerGeneration[unit])) { + kept.push_back({texture, unit, begin, end, texture->layerGeneration[unit], true, drawn}); + continue; + } + if (!drawn) continue; + const auto first = std::max(begin, address) / block; + const auto last = (std::min(end, address + bytes) - 1) / block; + const auto from = std::max(begin, first * block); + const auto to = std::min(end, (last + 1) * block); + std::vector generations(static_cast(last - first + 1)); + for (std::size_t k = 0; k < generations.size(); ++k) generations[k] = texture->unitBlockGeneration(unit, (first + k) * block); + std::vector changed(generations.size()); + if (!GuestMemory::ChangedBlocks(from, static_cast(to - from), generations, changed)) continue; + for (std::size_t k = 0; k < generations.size(); ++k) { + const auto at = (first + k) * block; + const auto blockFrom = std::max(at, begin); + const auto blockTo = std::min(at + block, end); + const bool edge = blockFrom != at || blockTo != at + block; + if (changed[k] == GuestMemory::BlockWritten && (!edge || !GuestMemory::UnchangedSince(blockFrom, static_cast(blockTo - blockFrom), generations[k]))) continue; + if (!kept.empty() && !kept.back().whole && kept.back().texture == texture && kept.back().unit == unit && kept.back().since == generations[k] && kept.back().to == blockFrom) kept.back().to = blockTo; + else kept.push_back({texture, unit, blockFrom, blockTo, generations[k], false, true}); + } + } const auto generation = stamp(); if (generation == 0 || kept.empty()) return generation; + std::vector confirmed; + for (const auto& entry : kept) { + if ((entry.whole && !entry.drawn) || GuestMemory::UnchangedSinceCollected(entry.from, static_cast(entry.to - entry.from), entry.since)) { + confirmed.push_back(entry); + continue; + } + if (entry.from / block == (entry.to - 1) / block) continue; + const auto last = (std::min(entry.to, address + bytes) - 1) / block; + for (auto at = std::max(entry.from, address) / block * block; at <= last * block; at += block) { + const auto from = std::max(at, entry.from); + const auto to = std::min(at + block, entry.to); + if (GuestMemory::UnchangedSinceCollected(from, static_cast(to - from), entry.since)) confirmed.push_back({entry.texture, entry.unit, from, to, entry.since, false, true}); + } + } auto& pending = Pending(); std::lock_guard lock(pending.mutex); - for (const auto& [texture, unit] : kept) { - if (!texture->layerPending[unit]) continue; - texture->layerGeneration[unit] = generation; - texture->refreshGeneration(); + for (const auto& entry : confirmed) { + auto& texture = *entry.texture; + if (!texture.layerPending[entry.unit]) continue; + if (entry.whole) { + texture.layerGeneration[entry.unit] = generation; + texture.refreshGeneration(); + continue; + } + const auto spanBegin = texture.descriptor.baseAddress / block; + if (texture.completionKept.empty()) texture.completionKept.assign(static_cast((texture.descriptor.baseAddress + texture.guestBytes - 1) / block - spanBegin + 1), 0); + for (auto at = entry.from / block; at <= (entry.to - 1) / block; ++at) texture.completionKept[static_cast(at - spanBegin)] = generation; } return generation; } @@ -5525,6 +5589,12 @@ void StorageTexture::blockGenerations(std::vector& generations) c } } +std::uint64_t StorageTexture::unitBlockGeneration(std::uint32_t unit, std::uint64_t at) const { + const auto unitGeneration = layerGeneration[unit]; + if (unitGeneration == 0 || !layerPending[unit] || completionKept.empty()) return unitGeneration; + return std::max(unitGeneration, completionKept[static_cast(at / 65536 - descriptor.baseAddress / 65536)]); +} + namespace { std::atomic& ClearHandOverOn() { @@ -5821,7 +5891,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { if (!layers[layer]) continue; const auto end = layerBegin(layer) + layerBytes(layer); - for (auto at = layerBegin(layer) & ~(block - 1); at < end; at += block) generations[static_cast((at - spanBegin) / block)] = layerGeneration[layer]; + for (auto at = layerBegin(layer) & ~(block - 1); at < end; at += block) generations[static_cast((at - spanBegin) / block)] = unitBlockGeneration(layer, at); } const auto drawn = drawnArea(); std::vector changedBlocks(spanBlocks); @@ -5864,7 +5934,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { const bool edge = from != at || to != at + block; const auto index = static_cast((at - spanBegin) / block); const bool kept = keptAtUnwatchHolds(from, layer); - const bool written = kept || unknown || (changedBlocks[index] == GuestMemory::BlockWritten && (compared[layer] || !edge || GuestMemory::StoredOver(from, static_cast(to - from), layerGeneration[layer])) && !unchangedSinceBaseline(from, to)); + const bool written = kept || unknown || (changedBlocks[index] == GuestMemory::BlockWritten && (compared[layer] || !edge || GuestMemory::StoredOver(from, static_cast(to - from), unitBlockGeneration(layer, at))) && !unchangedSinceBaseline(from, to)); if (written && (!drawn.rect || (tracked && !kept && !unknown && cpuBlocks[index] == 0))) { skippedAny = true; skippedLayer[layer] = true; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDirectWriteCompletion.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDirectWriteCompletion.cpp index ef4606f30a..4a10c86def 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDirectWriteCompletion.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetDirectWriteCompletion.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -55,9 +56,16 @@ constexpr std::uint32_t Untouched = 0x5a5a5a5au; constexpr std::uint32_t Filled = 0x9abcdef0u; constexpr std::uint32_t Drawn = 0xff0000ffu; constexpr std::uint32_t LabelValue = 0x600dcafeu; +constexpr std::uint32_t CpuStored = 0x0badf00du; constexpr std::uint32_t Rgba8UnormInfo = 0x28028u; constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t WaitEqual = 3; +constexpr std::uint32_t WaitAtLeast = 5; constexpr std::array, 2> Scissors{{{16, 8, 128, 96}, {128, 32, 240, 120}}}; +constexpr std::array BlockZeroScissor{24, 8, 232, 56}; +constexpr std::size_t CpuStoreOffset = BlockBytes + 32 * Pitch + 64 * 4; +static_assert(BlockZeroScissor[3] * Pitch <= BlockBytes); +static_assert(CpuStoreOffset >= BlockBytes && CpuStoreOffset < TargetBytes); alignas(256) constexpr std::array VertexCode{ 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, @@ -149,18 +157,22 @@ void AppendDispatch(std::vector& commands, const std::uint32_t* p commands.insert(commands.end(), {0xc0031500u, groups, 1, 1, 0x41u}); } -void AppendFill(std::vector& commands, std::uint64_t target, const std::uint32_t* copied) { - const auto direct = BufferDescriptor(target, TargetBytes); - const auto other = BufferDescriptor(reinterpret_cast(copied), TargetBytes); +void AppendFill(std::vector& commands, std::uint64_t target, const std::uint32_t* copied, std::size_t bytes) { + const auto direct = BufferDescriptor(target, bytes); + const auto other = BufferDescriptor(reinterpret_cast(copied), bytes); std::vector userData(direct.begin(), direct.end()); userData.insert(userData.end(), {Filled, Filled, Filled, Filled}); userData.insert(userData.end(), other.begin(), other.end()); - AppendDispatch(commands, PairFillCode.data(), userData, static_cast(TargetBytes / 1024u)); + AppendDispatch(commands, PairFillCode.data(), userData, static_cast(bytes / 1024u)); } -void AppendGate(std::vector& commands) { +void AppendLabel(std::vector& commands, std::uint64_t label) { + commands.insert(commands.end(), {0xc0064900u, 0x514u, 1u << 29u, static_cast(label), static_cast(label >> 32u), LabelValue, 0, 0}); +} + +void AppendGate(std::vector& commands, std::uint32_t function, std::uint32_t reference) { const auto gate = reinterpret_cast(&Gate); - commands.insert(commands.end(), {0xc0053c00u, 0x13u, static_cast(gate), static_cast(gate >> 32u), 1u, 0xffffffffu, 0x19u}); + commands.insert(commands.end(), {0xc0053c00u, 0x10u | function, static_cast(gate), static_cast(gate >> 32u), reference, 0xffffffffu, 0x19u}); } void Submit(std::vector& commands) { @@ -219,6 +231,7 @@ public: Region& operator=(const Region&) = delete; std::uint64_t Address() const { return reinterpret_cast(data); } std::uint32_t Word(std::size_t offset) const { return std::atomic_ref(*reinterpret_cast(data + offset)).load(std::memory_order_acquire); } + void Put(std::size_t offset, std::uint32_t value) const { std::atomic_ref(*reinterpret_cast(data + offset)).store(value, std::memory_order_release); } std::size_t bytes; private: @@ -261,6 +274,38 @@ std::string Mismatches(const Region& region, bool drawsLast) { return std::to_string(wrongCovered) + " of the " + std::to_string(covered) + " pixels the draws covered do not hold their color (" + std::to_string(filledCovered) + " hold the fill's pattern) and " + std::to_string(wrongElsewhere) + " of the " + elsewhere + " pixels outside them do not hold the fill's pattern"; } +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::string CpuStoreMismatches(const Region& region) { + std::size_t covered = 0, wrongCovered = 0, filledCovered = 0, others = 0, wrongOthers = 0; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto offset = y * Pitch + x * 4u; + if (offset == CpuStoreOffset) continue; + const auto value = region.Word(offset); + if (x >= BlockZeroScissor[0] && x < BlockZeroScissor[2] && y >= BlockZeroScissor[1] && y < BlockZeroScissor[3]) { + ++covered; + wrongCovered += value == Drawn ? 0u : 1u; + filledCovered += value == Filled ? 1u : 0u; + } else { + ++others; + wrongOthers += value == (offset < BlockBytes ? Filled : Untouched) ? 0u : 1u; + } + } + } + std::vector parts; + if (wrongCovered != 0) parts.push_back(std::to_string(wrongCovered) + " of the " + std::to_string(covered) + " pixels the draw covered in block 0 do not hold its color (" + std::to_string(filledCovered) + " hold the fill's pattern)"); + if (const auto stored = region.Word(CpuStoreOffset); stored != CpuStored) parts.push_back("the CPU store in block 1 holds " + Hex(stored) + ", not " + Hex(CpuStored)); + if (wrongOthers != 0) parts.push_back(std::to_string(wrongOthers) + " of the " + std::to_string(others) + " other pixels do not hold the fill's pattern in block 0 or the untouched pattern in block 1"); + std::string text; + for (const auto& part : parts) text += (text.empty() ? "" : "; ") + part; + return text; +} + bool Await(const std::function& done) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); while (!done()) { @@ -286,12 +331,11 @@ bool Run(Order order, const Region& region, std::vector& failures) } unregister{copied.data()}; Gate.store(0, std::memory_order_release); std::vector commands; - if (fillFirst) AppendFill(commands, target, copied.data()); + if (fillFirst) AppendFill(commands, target, copied.data(), TargetBytes); for (const auto& scissor : Scissors) AppendDraw(commands, target, scissor); - if (!fillFirst) AppendFill(commands, target, copied.data()); - const auto label = target + LabelOffset; - commands.insert(commands.end(), {0xc0064900u, 0x514u, 1u << 29u, static_cast(label), static_cast(label >> 32u), LabelValue, 0, 0}); - if (fillFirst) AppendGate(commands); + if (!fillFirst) AppendFill(commands, target, copied.data(), TargetBytes); + AppendLabel(commands, target + LabelOffset); + if (fillFirst) AppendGate(commands, WaitEqual, 1); Submit(commands); std::string failure; if (fillFirst) { @@ -310,6 +354,58 @@ bool Run(Order order, const Region& region, std::vector& failures) return true; } +bool RunCpuStore(bool watched, const Region& region, std::vector& failures) { + const std::string name = std::string("fill, draw in block 0, CPU store in block 1, ") + (watched ? "write-watched" : "unwatched") + " memory"; + const auto target = region.Address(); + Import(region); + if (watched && !AgcDriver::GuestMemory::Watched(target, TargetBytes)) return false; + if (!watched) { + AgcDriver::GuestMemory::Unwatch(target, region.bytes); + Require(!AgcDriver::GuestMemory::Watched(target, region.bytes), "linear target direct write completion: the region is still write-watched"); + } + std::vector copied(TargetBytes / 4u + 4u, Untouched); + GuestAllocations::Mutation().Add(copied.data(), TargetBytes + 16u, true, true, true); + struct Unregister { + void* data; + ~Unregister() { GuestAllocations::Mutation().Remove(data); } + } unregister{copied.data()}; + Gate.store(0, std::memory_order_release); + std::vector commands; + AppendFill(commands, target, copied.data(), BlockBytes); + AppendDraw(commands, target, BlockZeroScissor); + AppendGate(commands, WaitAtLeast, 1); + AppendLabel(commands, target + LabelOffset); + AppendGate(commands, WaitAtLeast, 2); + Submit(commands); + const auto completed = [&] { return std::atomic_ref(copied[0]).load(std::memory_order_acquire) == Filled; }; + bool pendingAtStore = false; + const bool recorded = Await([&] { + std::unique_lock hold(AgcDriver::GuestMemory::GpuMutex()); + if (AgcDriver::Graphics::StorageTexture::FindLive(target, TargetBytes) == nullptr) return false; + pendingAtStore = !completed(); + region.Put(CpuStoreOffset, CpuStored); + return true; + }); + Gate.store(1, std::memory_order_release); + std::string failure; + bool completedAtLabel = false; + if (!recorded) failure = "the draw into block 0 never made the target's image"; + else if (!Await([&] { return region.Word(LabelOffset) == LabelValue; })) failure = "the label after the draw never landed"; + else { + completedAtLabel = completed(); + failure = CpuStoreMismatches(region); + if (!failure.empty()) failure = "when the label after the draw landed: " + failure; + if (watched && (!pendingAtStore || !completedAtLabel)) failure += std::string(failure.empty() ? "" : "; ") + (pendingAtStore ? "the fill's completion had not run when the label landed" : "the fill's completion had run before the CPU store") + ", so the ordering under test did not occur"; + } + const bool watchedAtLabel = AgcDriver::GuestMemory::Watched(target, TargetBytes); + Gate.store(2, std::memory_order_release); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::Graphics::StorageTexture::FlushPending(target, region.bytes, nullptr, "test release"); + std::printf("linear target direct write completion, %s (watched at the label: %d, completion pending at the CPU store: %d, run by the label: %d): %s\n", name.c_str(), watchedAtLabel ? 1 : 0, pendingAtStore ? 1 : 0, completedAtLabel ? 1 : 0, failure.empty() ? "the draw's color and the CPU store" : failure.c_str()); + if (!failure.empty()) failures.push_back(name + ": " + failure); + return true; +} + } int main() { @@ -328,16 +424,21 @@ int main() { } shutdown; Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + const auto unwatchedImports = [] { + std::puts("linear target direct write completion tests skipped, the device does not keep host imports write-watched, so no completion stamp over the target is tracked"); + return VulkanTestSkipped; + }; std::vector failures; for (const auto order : {Order::FillFirst, Order::DrawsFirst}) { regions.push_back(std::make_unique(RegionBytes)); - if (!Run(order, *regions.back(), failures)) { - std::puts("linear target direct write completion tests skipped, the device does not keep host imports write-watched, so no completion stamp over the target is tracked"); - return VulkanTestSkipped; - } + if (!Run(order, *regions.back(), failures)) return unwatchedImports(); + } + for (const bool watched : {true, false}) { + regions.push_back(std::make_unique(RegionBytes)); + if (!RunCpuStore(watched, *regions.back(), failures)) return unwatchedImports(); } if (!failures.empty()) { - std::string message = "linear target direct write completion: at a completion point, the memory of a resident linear target did not hold the later of a direct buffer write with completion work and draws into it:"; + std::string message = "linear target direct write completion: at a completion point, the memory of a resident linear target did not hold the later of a direct buffer write with completion work and draws into it, or lost a CPU store made outside the draws:"; for (const auto& failure : failures) message += "\n " + failure; Require(false, message); } diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 204d189fdb..35da7ebae5 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -115,7 +115,7 @@ - [Draw input snapshots](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`. - [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver), except drawn resident linear targets (next entry), are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. Explicit-size file writes (`write`, `pwrite`, `writev`, `pwritev`, AIO and `fwrite`) synchronize their source ranges through the [host-read hook](../../../core/libs/prx/libc/src/GuestArena.cpp); other host readers are not covered. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default. - [Resident linear color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver): a CPU store inside a drawn scissor rectangle, on a pixel no primitive wrote, made before the target's store is overwritten by it, where the console keeps it. Recorded draws' results are stored before the graphics queue's next flip, label, end-of-pipe interrupt, drain or device wait, as the union of their scissor rectangles; a draw that brings a 17th rectangle that does not merge stores the target at once. A target with an attachment proxy, several mips or layers, a 3D or DCC surface, or results of a dispatch, fill or copy is stored after each draw. One that a fill or copy writes after its draws is stored whole at the next of those points, and GPU results in its CPU-written 64 KiB blocks are dropped. A store the draw pipeline's committer makes (label, end-of-pipe interrupt, device wait) skips a target whose range a queued draw still reads (`DrawPipeline::HeldReads`). -- [Direct writes over a drawn linear target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver): the draws recorded after a dispatch that wrote the target's memory in place and has completion work are dropped in the dispatch's range when anything else, such as a CPU store elsewhere in the target, changed the target's memory since its generation, where the console keeps them. `StampKeepingNewerResults` checks a drawn target as one unit; closing the gap needs a generation per 64 KiB block. +- [Direct writes over a drawn linear target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver): a CPU store into a 64 KiB block of a drawn resident linear target that lands while the completion of a direct write recorded before the draws checks and stamps that block (`StampKeepingNewerResults`) drops the draws' results in that block, where the console keeps them: the stamp's own collect records the store per page only, so the store reads the block as written by the driver. - [`APS5_SKIP_REFUSED_DRAWS`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp) (libSceAgcDriver) drops a draw whose graphics stage the recompiler refuses instead of failing the queue, so the frame lacks that draw, and the stage is compiled and refused again at each such draw. Compute refusals still fail. - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending saves its bytes through the unwatch hook, which also runs when a failed collect takes a range out of the watch. A range that leaves the watch without the hook running over it (an import fallback that unwatched without saving) leaves its pending results without saved bytes; storing such results throws. A unit whose results were stored while its blocks were watched keeps only a generation; when its blocks leave the watch while nothing is pending, the hook saves nothing, and results that land in it afterwards have neither saved bytes nor a watch, so storing them throws too. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory when the import probe keeps the write watch and a settle queue exists: those of 1 MiB or more stay watched, and smaller ones return to the watch once their settle has completed, unless a storage image overlaps them (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. From 928729bdfbbb8dcac49a087b8b631963d75af003 Mon Sep 17 00:00:00 2001 From: hypermist Date: Sat, 24 Oct 2026 22:02:15 +1300 Subject: [PATCH 186/428] feat(libSceFont): report the glyph outline profile through the FreeType driver sceFontGetFontGlyphsOutlineProfile reports the maxp maximum contours, points, composite contours and composite points through the driver slot at SysDriver+0x70, as libSceFont and libSceFontFt do in firmware 13.60. A maxp older than 1.0 returns SCE_FONT_ERROR_NO_SUPPORT_FUNCTION, and a failed call clears the output. Font-set records, which the console merges over system font entries, still throw. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceFont/CMakeLists.txt | 9 ++ .../prx/libSceFont/include/FontDriver.hpp | 4 +- .../prx/libSceFont/include/FontInternal.hpp | 1 + .../libs/prx/libSceFont/include/FontTypes.hpp | 8 ++ core/libs/prx/libSceFont/src/Layout.cpp | 32 +++++ core/libs/prx/libSceFont/src/Render.cpp | 16 +++ .../libs/prx/libSceFont/src/Unimplemented.cpp | 6 - core/libs/prx/libSceFontFt/src/Driver.cpp | 14 ++ core/libs/tests/GuestFontOutlineProfile.cpp | 128 ++++++++++++++++++ docs/dev/debt/libSceFont.md | 3 +- 10 files changed, 213 insertions(+), 8 deletions(-) create mode 100644 core/libs/tests/GuestFontOutlineProfile.cpp diff --git a/core/libs/prx/libSceFont/CMakeLists.txt b/core/libs/prx/libSceFont/CMakeLists.txt index 3e9576b964..9d7e177cb1 100644 --- a/core/libs/prx/libSceFont/CMakeLists.txt +++ b/core/libs/prx/libSceFont/CMakeLists.txt @@ -34,6 +34,15 @@ if(BUILD_TESTING) set_tests_properties(guest_font_style_information PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_font_outline_profile_tests ${LIBS_INCLUDE_DIR}/tests/GuestFontOutlineProfile.cpp) + target_include_directories(guest_font_outline_profile_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_font_outline_profile_tests PRIVATE libSceFont libSceFontFt libc) + configure_windows_unwind(guest_font_outline_profile_tests) + add_test(NAME guest_font_outline_profile COMMAND guest_font_outline_profile_tests) + if(WIN32) + set_tests_properties(guest_font_outline_profile PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_font_writing_line_tests ${LIBS_INCLUDE_DIR}/tests/GuestFontWritingLine.cpp) target_include_directories(guest_font_writing_line_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_font_writing_line_tests PRIVATE libSceFont libc) diff --git a/core/libs/prx/libSceFont/include/FontDriver.hpp b/core/libs/prx/libSceFont/include/FontDriver.hpp index d766c4209f..70b562b215 100644 --- a/core/libs/prx/libSceFont/include/FontDriver.hpp +++ b/core/libs/prx/libSceFont/include/FontDriver.hpp @@ -209,6 +209,7 @@ using DriverGlyphIndexFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_ 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 DriverFontMetricsFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t form, FontMetrics* outMetrics); +using DriverOutlineProfileFunction = int (APS5_VABI*)(FontObj* fontObj, FontOutlineProfile* outProfile); using DriverStyleInformationFunction = int (APS5_VABI*)(FontObj* fontObj, FontStyleInformation* outInformation); 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); @@ -232,7 +233,7 @@ struct SysDriver { std::uint8_t reserved_0x58[0x08]; DriverMetricFunction metric; DriverGlyphsCountFunction glyphs_count; - std::uint8_t reserved_0x70[0x08]; + DriverOutlineProfileFunction outline_profile; DriverGlyphIndexFunction glyph_index; DriverSetCharWithDpiFunction set_char_with_dpi; DriverSetCharDefaultDpiFunction set_char_default_dpi; @@ -346,6 +347,7 @@ static_assert(offsetof(SysDriver, pixel_resolution) == 0x10); static_assert(offsetof(SysDriver, open) == 0x38); static_assert(offsetof(SysDriver, style_information) == 0x48); static_assert(offsetof(SysDriver, scale) == 0x50); +static_assert(offsetof(SysDriver, outline_profile) == 0x70); static_assert(offsetof(SysDriver, glyph_index) == 0x78); static_assert(offsetof(SysDriver, font_metrics) == 0x98); static_assert(offsetof(SysDriver, compute_layout) == 0xA0); diff --git a/core/libs/prx/libSceFont/include/FontInternal.hpp b/core/libs/prx/libSceFont/include/FontInternal.hpp index 188151458c..da4a70b28a 100644 --- a/core/libs/prx/libSceFont/include/FontInternal.hpp +++ b/core/libs/prx/libSceFont/include/FontInternal.hpp @@ -135,6 +135,7 @@ int GetCharGlyphMetrics(FontHandle handle, std::uint32_t code, FontGlyphMetrics* int RenderCharGlyphImageCore(FontHandle handle, std::uint32_t code, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* result); int ComputeFontMetrics(FontHandle handle, std::uint32_t form, FontMetrics* metrics); int ComputeFontStyleInformation(FontHandle handle, FontStyleInformation* information); +int ComputeFontGlyphsOutlineProfile(FontHandle handle, FontOutlineProfile* profile); } diff --git a/core/libs/prx/libSceFont/include/FontTypes.hpp b/core/libs/prx/libSceFont/include/FontTypes.hpp index 1fab30eefc..6507485349 100644 --- a/core/libs/prx/libSceFont/include/FontTypes.hpp +++ b/core/libs/prx/libSceFont/include/FontTypes.hpp @@ -442,6 +442,13 @@ struct FontMetrics { float slantRatio; }; +struct FontOutlineProfile { + std::uint32_t maxContours; + std::uint32_t maxPoints; + std::uint32_t maxCompositeContours; + std::uint32_t maxCompositePoints; +}; + struct FontStyleInformation { const char* familyName; const char* styleName; @@ -463,6 +470,7 @@ static_assert(offsetof(FontMetrics, xMin) == 0x1C); static_assert(offsetof(FontMetrics, emWidth) == 0x2C); static_assert(offsetof(FontMetrics, effectWeightX) == 0x34); static_assert(sizeof(FontStyleInformation) == 0x18); +static_assert(sizeof(FontOutlineProfile) == 0x10); static_assert(offsetof(FontStyleInformation, styleFlags) == 0x10); static_assert(sizeof(FontGlyphImageMetrics) == 0x18); static_assert(sizeof(FontGenerateGlyphDetail) == 0x20); diff --git a/core/libs/prx/libSceFont/src/Layout.cpp b/core/libs/prx/libSceFont/src/Layout.cpp index edb6e02d58..b013c7c79f 100644 --- a/core/libs/prx/libSceFont/src/Layout.cpp +++ b/core/libs/prx/libSceFont/src/Layout.cpp @@ -600,3 +600,35 @@ int Font::ComputeFontStyleInformation(FontHandle handle, FontStyleInformation* i ReleaseFontCtxEntryLock(entry, modeLow, lockWord); return rc; } + +int Font::ComputeFontGlyphsOutlineProfile(FontHandle handle, FontOutlineProfile* profile) { + auto* font = GetNativeFont(handle); + if (!profile) return SCE_FONT_ERROR_INVALID_PARAMETER; + auto* library = static_cast(font->library); + auto* context = library ? FontContext(library, font) : nullptr; + if (!context) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + *profile = {}; + const std::uint32_t fontId = font->open_info.ctx_entry_index; + if (static_cast(fontId) < 0) return SCE_FONT_OK; + const SysDriver* driver = library->sys_driver; + if (!driver || !driver->outline_profile) return SCE_FONT_ERROR_FATAL; + const std::uint32_t modeLow = font->flags & 0x0Fu; + FontObj* head = nullptr; + std::uint32_t lockWord = 0; + auto* entry = AcquireFontCtxEntry(context, fontId, modeLow, &head, &lockWord); + if (!entry) return SCE_FONT_ERROR_FATAL; + FontObj* match = FindSubFont(head, font->open_info.sub_font_index); + int rc = SCE_FONT_ERROR_FATAL; + FontOutlineProfile faceProfile{}; + if ((lockWord & OPEN_BIT) != 0 && (lockWord & COUNT_MASK) != 0 && match) { + match->font_handle = handle; + rc = driver->outline_profile(match, &faceProfile); + } + ReleaseFontCtxEntryLock(entry, modeLow, lockWord); + if (rc != SCE_FONT_OK) return rc; + profile->maxContours = std::max(profile->maxContours, faceProfile.maxContours); + profile->maxPoints = std::max(profile->maxPoints, faceProfile.maxPoints); + profile->maxCompositeContours = std::max(profile->maxCompositeContours, faceProfile.maxCompositeContours); + profile->maxCompositePoints = std::max(profile->maxCompositePoints, faceProfile.maxCompositePoints); + return SCE_FONT_OK; +} diff --git a/core/libs/prx/libSceFont/src/Render.cpp b/core/libs/prx/libSceFont/src/Render.cpp index 7e2871aeba..705b0c0628 100644 --- a/core/libs/prx/libSceFont/src/Render.cpp +++ b/core/libs/prx/libSceFont/src/Render.cpp @@ -198,6 +198,22 @@ int APS5_VABI sceFontGetFontStyleInformation(FontHandle fontHandle, FontStyleInf return rc; } +int APS5_VABI sceFontGetFontGlyphsOutlineProfile(FontHandle fontHandle, FontOutlineProfile* profile) { + auto* font = GetNativeFont(fontHandle); + std::uint32_t fontLock = 0; + int rc = SCE_FONT_ERROR_INVALID_FONT_HANDLE; + if (font && font->magic == HANDLE_MAGIC) { + if (font->open_info.fontset_record) NotImplemented_nid_no_patch(__func__); + if (AcquireFontLock(font, fontLock)) { + rc = ComputeFontGlyphsOutlineProfile(fontHandle, profile); + ReleaseFontLock(font, fontLock); + if (rc == SCE_FONT_OK) return rc; + } + } + if (profile) *profile = {}; + return rc; +} + int APS5_VABI sceFontGetVerticalLayout(FontHandle fontHandle, FontVerticalLayout* layout) { int rc = SCE_FONT_ERROR_INVALID_FONT_HANDLE; auto* font = GetNativeFont(fontHandle); diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index 5bb7e6fdd3..898a8c96b2 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -10,7 +10,6 @@ struct FontGraphicsService; struct FontGraphicsServiceParam; struct FontWords; struct FontGlyphSupport; -struct FontOutlineProfile; struct FontGlyphExpandBufferState; struct FontGraphicsResource; struct FontGraphicsFrame; @@ -177,11 +176,6 @@ int APS5_VABI sceFontDestroyWords(FontWords** words) { } -int APS5_VABI sceFontGetFontGlyphsOutlineProfile(FontHandle font, FontOutlineProfile* profile) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontGetGlyphExpandBufferState(FontHandle font, FontGlyphExpandBufferState* state) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceFontFt/src/Driver.cpp b/core/libs/prx/libSceFontFt/src/Driver.cpp index a87e859fdd..cc49df9835 100644 --- a/core/libs/prx/libSceFontFt/src/Driver.cpp +++ b/core/libs/prx/libSceFontFt/src/Driver.cpp @@ -881,6 +881,19 @@ int APS5_VABI LibraryGetFontMetrics(FontObj* obj, std::uint32_t form, FontMetric return SCE_FONT_OK; } +int APS5_VABI LibraryGetOutlineProfile(FontObj* obj, FontOutlineProfile* profile) { + const FT_Face face = obj ? FaceOf(obj) : nullptr; + if (!face) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + const auto* maxp = static_cast(FT_Get_Sfnt_Table(face, FT_SFNT_MAXP)); + if (!maxp) throw std::runtime_error("FontFt: outline profile of a face without a maxp table"); + if (maxp->version < 0x10000) return SCE_FONT_ERROR_NO_SUPPORT_FUNCTION; + profile->maxContours = maxp->maxContours; + profile->maxPoints = maxp->maxPoints; + profile->maxCompositeContours = maxp->maxCompositeContours; + profile->maxCompositePoints = maxp->maxCompositePoints; + return SCE_FONT_OK; +} + std::uint16_t WeightClass(std::uint16_t weight) { return weight >= 100 && weight <= 900 && weight % 100 == 0 ? static_cast(weight / 100) : 0; } @@ -917,6 +930,7 @@ SysDriver MakeDriverTable() { driver.set_char_default_dpi = &LibrarySetCharSizeDefaultDpi; driver.font_metrics = &LibraryGetFontMetrics; driver.style_information = &LibraryGetFontStyleInformation; + driver.outline_profile = &LibraryGetOutlineProfile; driver.compute_layout = &LibraryComputeLayout; driver.compute_layout_alt = &LibraryComputeLayoutAlt; driver.load_glyph_cached = &LibraryLoadGlyphCached; diff --git a/core/libs/tests/GuestFontOutlineProfile.cpp b/core/libs/tests/GuestFontOutlineProfile.cpp new file mode 100644 index 0000000000..3d9cca0efb --- /dev/null +++ b/core/libs/tests/GuestFontOutlineProfile.cpp @@ -0,0 +1,128 @@ +#include "prx/libSceFont/include/FontDriver.hpp" +#include "tests/FontFixture.hpp" +#include +#include +#include +#include +#include +#include +#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 sceFontSupportExternalFonts(FontLibrary, std::uint32_t, std::uint32_t); +int APS5_VABI sceFontOpenFontMemory(FontLibrary, const void*, std::uint32_t, const FontOpenDetail*, FontHandle*); +int APS5_VABI sceFontCloseFont(FontHandle); +int APS5_VABI sceFontGetFontGlyphsOutlineProfile(FontHandle, FontOutlineProfile*); +const void* APS5_VABI sceFontSelectLibraryFt(int); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Font outline profile check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static void* APS5_VABI Allocate(void*, std::uint32_t size) { + return std::malloc(size); +} + +static void APS5_VABI Release(void*, void* pointer) { + std::free(pointer); +} + +static void PatchMaxp(std::vector& font, std::initializer_list words) { + const int tableCount = (font[4] << 8) | font[5]; + for (int i = 0; i < tableCount; ++i) { + const std::size_t entry = 12 + 16 * static_cast(i); + if (std::memcmp(font.data() + entry, "maxp", 4) != 0) continue; + std::size_t offset = (static_cast(font[entry + 8]) << 24) | (static_cast(font[entry + 9]) << 16) | + (static_cast(font[entry + 10]) << 8) | font[entry + 11]; + for (const int word : words) { + if (word >= 0) { + font[offset] = static_cast((word >> 8) & 0xFF); + font[offset + 1] = static_cast(word & 0xFF); + } + offset += 2; + } + return; + } + Require(false); +} + +static FontHandle Open(FontLibrary library, const std::vector& data) { + FontHandle font = nullptr; + Require(sceFontOpenFontMemory(library, data.data(), static_cast(data.size()), nullptr, &font) == SCE_FONT_OK && font != nullptr); + return font; +} + +static void Fill(FontOutlineProfile& profile) { + std::memset(&profile, 0x7F, sizeof(profile)); +} + +static bool Zero(const FontOutlineProfile& profile) { + return profile.maxContours == 0 && profile.maxPoints == 0 && profile.maxCompositeContours == 0 && profile.maxCompositePoints == 0; +} + +int main() { + const FontMemoryInterface iface{Allocate, Release, nullptr, nullptr, nullptr, nullptr}; + FontMemory memory{}; + Require(sceFontMemoryInit(&memory, nullptr, 0, &iface, nullptr, nullptr, nullptr) == SCE_FONT_OK); + FontLibrary library = nullptr; + Require(sceFontCreateLibrary(&memory, sceFontSelectLibraryFt(0), &library) == SCE_FONT_OK); + Require(sceFontSupportExternalFonts(library, 4, 0x52) == SCE_FONT_OK); + + auto profiledData = FontFixture::BuildFont(1, {}); + PatchMaxp(profiledData, {-1, -1, -1, 120, 7, 300, 11}); + FontHandle profiled = Open(library, profiledData); + FontOutlineProfile profile{}; + Fill(profile); + Require(sceFontGetFontGlyphsOutlineProfile(profiled, &profile) == SCE_FONT_OK); + Require(profile.maxContours == 7 && profile.maxPoints == 120 && profile.maxCompositeContours == 11 && profile.maxCompositePoints == 300); + + const auto plainData = FontFixture::BuildFont(1, {}); + FontHandle plain = Open(library, plainData); + Fill(profile); + Require(sceFontGetFontGlyphsOutlineProfile(plain, &profile) == SCE_FONT_OK && Zero(profile)); + + auto shortData = FontFixture::BuildFont(1, {}); + PatchMaxp(shortData, {0, 0x5000}); + FontHandle shortMaxp = Open(library, shortData); + Fill(profile); + Require(sceFontGetFontGlyphsOutlineProfile(shortMaxp, &profile) == SCE_FONT_ERROR_NO_SUPPORT_FUNCTION && Zero(profile)); + + std::array record{}; + auto* nativeFont = reinterpret_cast(profiled); + nativeFont->open_info.fontset_record = record.data(); + bool recordThrew = false; + try { + sceFontGetFontGlyphsOutlineProfile(profiled, &profile); + } catch (const std::runtime_error&) { + recordThrew = true; + } + nativeFont->open_info.fontset_record = nullptr; + Require(recordThrew); + Fill(profile); + Require(sceFontGetFontGlyphsOutlineProfile(profiled, &profile) == SCE_FONT_OK && profile.maxPoints == 120); + + Fill(profile); + Require(sceFontGetFontGlyphsOutlineProfile(nullptr, &profile) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && Zero(profile)); + std::uint64_t notAFont[32]{}; + Fill(profile); + Require(sceFontGetFontGlyphsOutlineProfile(reinterpret_cast(notAFont), &profile) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && Zero(profile)); + Require(sceFontGetFontGlyphsOutlineProfile(plain, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontGetFontGlyphsOutlineProfile(nullptr, nullptr) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + + for (FontHandle font : {profiled, plain, shortMaxp}) Require(sceFontCloseFont(font) == SCE_FONT_OK); + Require(sceFontDestroyLibrary(&library) == SCE_FONT_OK); + Require(sceFontMemoryTerm(&memory) == SCE_FONT_OK); + std::puts("Font outline profile tests passed"); + return 0; +} diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index 0384b89ce5..6ccf8b12d8 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -24,9 +24,10 @@ - [sceFontGetCharGlyphMetrics, sceFontGetRenderCharGlyphMetrics](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) report the vertical metrics the way the [FreeType driver](../../../core/libs/prx/libSceFontFt/src/Driver.cpp) reports them for a rendered glyph, not confirmed on a title: FreeType's vertical advance, and for a face without vertical metrics a bearing of half the horizontal advance left of the horizontal bearing and of half the advance left over by the glyph height. - [sceFontGetFontMetrics](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) - the 64-byte output, the form fallbacks (2 and 3 fall back to the `hhea` values when OS/2 is missing or gives an empty range, 4 builds vertical values from `vhea` or OS/2) and the raw weight deltas and slant copied from the style are read from the disassembly of firmware 13.60 `libSceFont.sprx` and `libSceFontFt.sprx`, not measured on a console. For a font set the values are those of its primary file; which file the console reports is not recovered. Closes when: measured on a PS5 - [sceFontGetFontStyleInformation](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) - the 24-byte output (FreeType family and style names, italic and bold flags, OS/2 weight class when it is a multiple of 100 from 100 to 900, width class below 10, and 1 plus a right-to-left `head` direction plus 0x80 for vertical metrics) and the font-set record path are read from the disassembly of firmware 13.60 `libSceFont.sprx` and `libSceFontFt.sprx`, not measured on a console. A face without a `head` table, such as a BDF font, throws: the console reads the table without checking it. Closes when: measured on a PS5 +- [sceFontGetFontGlyphsOutlineProfile](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) - the 16-byte output (the `maxp` maximum contours, points, composite contours and composite points) and `SCE_FONT_ERROR_NO_SUPPORT_FUNCTION` for a `maxp` older than 1.0 are read from the disassembly of firmware 13.60 `libSceFont.sprx` and `libSceFontFt.sprx`, not measured on a console. A handle with a font-set record throws: the console takes the maximum over the record's system font entries, which are not modelled. A face without a `maxp` table throws, as the console reads it unchecked. Closes when: measured on a PS5 - [Font writing lines](../../../core/libs/prx/libSceFont/src/WritingLine.cpp): visual ordering and spacing originated from PS4 measurements and match the supplied PS5 structure and control flow. Exhaustive floating-point equivalence, including NaN payloads and rounding, has not been verified against a running PS5 module. -- [Font graphics and metric signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): graphics-device/service creation takes font memory, parameters and a handle output; destruction takes a handle pointer. Attribute selectors and outputs are 32-bit values. Outline-profile and expansion-buffer outputs remain opaque pending field recovery. Protected implementations are unchanged. +- [Font graphics and metric signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): graphics-device/service creation takes font memory, parameters and a handle output; destruction takes a handle pointer. Attribute selectors and outputs are 32-bit values. Expansion-buffer outputs remain opaque pending field recovery. Protected implementations are unchanged. - [Font graphics fill and transform signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): region layout and scalar float arguments are confirmed from assembly, including the extra 32-bit corner flags omitted by the roundish-region decompilation. Fill-method, plot and rates layouts, layout selector meanings and mapping parameter meanings remain unresolved. Protected bodies are unchanged. From 29a6fc7169983f1b25a79d31c2a9efb9ab011cec Mon Sep 17 00:00:00 2001 From: Nawasfadili Date: Wed, 14 Oct 2026 21:32:50 +0000 Subject: [PATCH 187/428] fix(libkernel): retire an EV_ONESHOT note through its delete callback and follow Windows $Deleted paths Carries the additive pieces of #6510 onto main after its feature was superseded by #5847: the deleteEventFunc invocation when an EV_ONESHOT note retires in CollectTriggered, the Windows $Deleted path detection in FileEvent (IsDeleted transitions), the relaxed timer-modify and SetVnodeNotes guards, and the compatible kevent tests. --- core/libs/prx/libkernel/Equeue/Equeue.cpp | 4 + .../prx/libkernel/Equeue/src/FileEvent.cpp | 4 +- core/libs/prx/libkernel/Equeue/src/Kqueue.cpp | 2 +- core/libs/tests/GuestKqueue.cpp | 85 +++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 5 files changed, 93 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/Equeue/Equeue.cpp b/core/libs/prx/libkernel/Equeue/Equeue.cpp index 1256cc9391..aad2915831 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.cpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.cpp @@ -197,6 +197,10 @@ int KernelEqueuePrivate::CollectTriggered(KernelEvent* ev, int num) { while (e.triggered && ret < num) { ev[ret++] = e.event; if ((e.event.flags & EV_ONESHOT) != 0) { + if (e.filter.deleteEventFunc != nullptr) { + auto owner = e.filter.owner; + e.filter.deleteEventFunc(m_handle, &e); + } erase = true; break; } diff --git a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp index cd24ff30d0..3300e9d8df 100644 --- a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp +++ b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp @@ -192,7 +192,7 @@ bool TakeSnapshot(HANDLE handle, Snapshot& snapshot) { } bool IsDeleted(const Snapshot& snapshot) { - return snapshot.deletePending || snapshot.links == 0; + return snapshot.deletePending || snapshot.links == 0 || snapshot.path.find(L"\\$Extend\\$Deleted\\") != std::wstring::npos; } std::vector SnapshotHints(const Snapshot& before, const Snapshot& after, bool directory) { @@ -212,7 +212,7 @@ std::vector SnapshotHints(const Snapshot& before, const Snapshot& } else if (!directory && hints.empty() && after.change != before.change) { hints.push_back(NOTE_ATTRIB); } - if ((after.deletePending && !before.deletePending) || after.links < before.links) { + if ((after.deletePending && !before.deletePending) || after.links < before.links || (!IsDeleted(before) && IsDeleted(after))) { hints.push_back(NOTE_DELETE); } if (!after.path.empty() && after.path != before.path && !IsDeleted(after)) { diff --git a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp index 917bc4a8f8..3a5bb54cc1 100644 --- a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp +++ b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp @@ -328,7 +328,7 @@ void TouchUserKnote(KernelEqueueEvent& event, KernelEvent& change) { } int ModifyKnote(const KernelEqueueRef& queue, KernelEvent& change) { - if (change.filter == EVFILT_TIMER) { + if (change.filter == EVFILT_TIMER && ((change.flags & EV_ADD) != 0 || change.data != 0)) { NotImplemented_nid_no_patch("kevent: changing an existing EVFILT_TIMER"); } const bool descriptor = change.filter == EVFILT_READ || change.filter == EVFILT_WRITE; diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index fd0acdad4e..6fbce195e6 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -1,5 +1,6 @@ #include "SceTypes.hpp" #include +#include #include #include #include @@ -7,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -1107,6 +1109,84 @@ static void DeniedParentWatch() { Require(std::filesystem::remove_all(path) == 2); } #endif + +#ifndef _WIN32 +static std::size_t InotifyWatchCount() { + std::size_t count = 0; + std::error_code error; + for (const auto& entry : std::filesystem::directory_iterator("/proc/self/fd", error)) { + std::error_code linkError; + const auto target = std::filesystem::read_symlink(entry.path(), linkError); + if (!linkError && target.string().find("inotify") != std::string::npos) ++count; + } + return count; +} +#endif + +static void VnodeOneshotWatchStops() { + const auto path = TemporaryName("oneshot"); + const int file = open_nid_postfix(path.c_str(), ReadWrite | Create, 0644); + Require(file >= 0); + const int kq = kqueue_nid_postfix(); + int marker = 0; + KernelEvent events[4]{}; + const auto ident = static_cast(file); +#ifndef _WIN32 + const auto watchesBefore = InotifyWatchCount(); +#endif + Require(Apply(kq, Change(ident, EvfiltVnode, EvAdd | EvOneshot, NoteDelete | NoteWrite, 0, &marker)) == 0); + Require(write_nid_postfix(file, "abc", 3) == 3); + Require(Wait(kq, events, 4) == 1); + Require(events[0].ident == ident && events[0].filter == EvfiltVnode && (events[0].flags & EvOneshot) != 0 && events[0].udata == &marker); + // The retired note stopped its watch: further writes produce nothing and the note is gone. + Require(write_nid_postfix(file, "def", 3) == 3); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + Require(Poll(kq, events, 4) == 0); + RequireFailure(Apply(kq, Change(ident, EvfiltVnode, EvDelete)), GuestEnoent); +#ifndef _WIN32 + // The retired oneshot releases its host watch rather than leaving it running: the inotify + // descriptor count returns to what it was before the watch (each retired oneshot used to + // leak its detached watch thread). + for (int attempt = 0; attempt < 100 && InotifyWatchCount() != watchesBefore; ++attempt) std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(InotifyWatchCount() == watchesBefore); +#endif + Require(close_nid_postfix(file) == 0); + Require(close_nid_postfix(kq) == 0); + Require(std::filesystem::remove(path)); +} + +static void PeriodicTimerFiresTwice() { + const int kq = kqueue_nid_postfix(); + KernelEvent events[4]{}; + Require(Apply(kq, Change(1, EvfiltTimer, EvAdd, NoteUseconds, 50000)) == 0); + Require(Wait(kq, events, 4) == 1); + Require(Wait(kq, events, 4) == 1); + Require(Apply(kq, Change(1, EvfiltTimer, EvDelete)) == 0); + Require(close_nid_postfix(kq) == 0); +} + +#ifdef _WIN32 +static void RecycleBinDeleteWatchedFile() { + // Windows deletes surface as a rename into the POSIX recycle bin; a path under + // \\$Extend\\$Deleted\\ is a delete for the watch. + const auto directory = TemporaryName("deleted"); + std::filesystem::create_directories(directory + "\\$Extend\\$Deleted"); + const auto path = directory + "\\watched"; + const int file = open_nid_postfix(path.c_str(), ReadWrite | Create, 0644); + Require(file >= 0); + const int kq = kqueue_nid_postfix(); + KernelEvent events[4]{}; + const auto ident = static_cast(file); + Require(Apply(kq, Change(ident, EvfiltVnode, EvAdd | EvClear, NoteDelete | NoteRename)) == 0); + Require(sceKernelRename(path.c_str(), (directory + "\\$Extend\\$Deleted\\moved").c_str()) == SceOk); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + Require(Poll(kq, events, 4) == 1 && events[0].ident == ident && (events[0].fflags & NoteDelete) != 0); + Require(close_nid_postfix(file) == 0); + Require(close_nid_postfix(kq) == 0); + std::filesystem::remove_all(directory); +} +#endif + static void Timeout() { const int kq = kqueue_nid_postfix(); KernelEvent event{}; @@ -1205,6 +1285,11 @@ int main() { FileWatch(); #ifndef _WIN32 RenameAndDeleteWatchedFile(); + VnodeOneshotWatchStops(); + PeriodicTimerFiresTwice(); +#endif +#ifdef _WIN32 + RecycleBinDeleteWatchedFile(); #endif #if !defined(_WIN32) && !defined(__APPLE__) MoveDirectoryEntries(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 5caf73536b..db1c65a38f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -273,3 +273,4 @@ - [sysctl MIBs for kern.smp.maxcpus, kern.stackprot, kern.sched.name and kern.timecounter.hardware](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - on a PS5 (firmware 12.70), a payload process resolves these names to {1, 2147482816, 2147482814}, {1, 2147483151}, {1, 2147483182, 2147483181} and {1, 2147482838, 2147482834}; `sysctl` returns -1 with `ENOENT` for reads and size queries by those exact MIBs, leaving the buffer and length unchanged, including when either or both output pointers are null. Writes were not measured and throw. `sysctlnametomib` still throws for these four names; #7541 implements the mappings for nine of the other names. - [sceKernelGetProcessAbiType](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) throws: no measured value, prototype or documented consumer of the process ABI type was found, and PS5_MihawkOS (`mihawk-99/PS5_MihawkOS`, PPSA99170) is the only title known to import it. Returning a guessed constant would let a title branch on a value the console never answered with, so the declaration only resolves the import until the console value is measured - [flock on a directory descriptor](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) returns 0 on Windows without taking a lock, as FreeBSD 11.4's `sys_flock` accepts any vnode descriptor and so succeeds on a directory; Windows `LockFileEx` rejects a directory handle, which used to abort the process. Two descriptors of the same directory therefore never conflict: a second `LOCK_EX | LOCK_NB` succeeds where FreeBSD fails with `EWOULDBLOCK`. Linux and macOS use the host `flock`. Not measured on a console +- [EVFILT_TIMER modify with data 0](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - a timer change with `EV_ADD` unset and `data == 0` keeps the note's period instead of taking FreeBSD's zero default, which would fire without delay; whether the console keeps or zeroes the period is unverified. From 13934baea2c4d9a16e74ccfc8a121fc43bbbd109 Mon Sep 17 00:00:00 2001 From: blawesom Date: Wed, 21 Oct 2026 19:10:45 +0200 Subject: [PATCH 188/428] test(libkernel): gate the oneshot leak check on Linux and drop the superseded kevent tests - RecycleBinDeleteWatchedFile now runs on Windows: its call moves out of the #ifndef _WIN32 block so the $Extend$Deleted$ path is exercised - InotifyWatchCount and the two leak assertions gate on __linux__ (macOS has no /proc; the count was always 0 and the assertion vacuous) - DisableAndEnable, Dispatch, ForceOneshot and StressTest drop with their main() calls: they belong to the superseded #6510 feature, and StressTest asserts strict arrival order across 4 threads with 1-2 ms sleeps (CI flake risk) --- core/libs/tests/GuestKqueue.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index 6fbce195e6..dc8b248779 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -1,6 +1,5 @@ #include "SceTypes.hpp" #include -#include #include #include #include @@ -8,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -1110,7 +1108,7 @@ static void DeniedParentWatch() { } #endif -#ifndef _WIN32 +#ifdef __linux__ static std::size_t InotifyWatchCount() { std::size_t count = 0; std::error_code error; @@ -1131,7 +1129,7 @@ static void VnodeOneshotWatchStops() { int marker = 0; KernelEvent events[4]{}; const auto ident = static_cast(file); -#ifndef _WIN32 +#ifdef __linux__ const auto watchesBefore = InotifyWatchCount(); #endif Require(Apply(kq, Change(ident, EvfiltVnode, EvAdd | EvOneshot, NoteDelete | NoteWrite, 0, &marker)) == 0); @@ -1143,7 +1141,7 @@ static void VnodeOneshotWatchStops() { std::this_thread::sleep_for(std::chrono::milliseconds(300)); Require(Poll(kq, events, 4) == 0); RequireFailure(Apply(kq, Change(ident, EvfiltVnode, EvDelete)), GuestEnoent); -#ifndef _WIN32 +#ifdef __linux__ // The retired oneshot releases its host watch rather than leaving it running: the inotify // descriptor count returns to what it was before the watch (each retired oneshot used to // leak its detached watch thread). From 681cfa117051c9e47e046685e1458f147f75a60d Mon Sep 17 00:00:00 2001 From: blawesom Date: Thu, 22 Oct 2026 19:26:41 +0200 Subject: [PATCH 189/428] fix(libkernel): refuse a timer modify with fflags and pin the disabled timer's enable re-arm - a timer change with EV_ADD unset and data 0 keeps the period; one that set fflags was silently ignored, where FreeBSD 11's kqueue_register copies it onto the note: it now throws with the other re-adds, waiting for the measured rules - EV_ENABLE on a disabled timer reports the expiries missed while disabled in one step and keeps the note's phase, as FreeBSD 11's filt_timerexpire counts a disabled note and keeps its p_nexttime; TimerDisableEnableRearm pins it (5 serial runs and 3 rounds of 8 parallel copies pass) - the next-wait abort from the review round was observed before main's 5cbc46029b wait refactor and does not reproduce --- core/libs/prx/libkernel/Equeue/src/Kqueue.cpp | 2 +- core/libs/tests/GuestKqueue.cpp | 19 +++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp index 3a5bb54cc1..0644aaaca1 100644 --- a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp +++ b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp @@ -328,7 +328,7 @@ void TouchUserKnote(KernelEqueueEvent& event, KernelEvent& change) { } int ModifyKnote(const KernelEqueueRef& queue, KernelEvent& change) { - if (change.filter == EVFILT_TIMER && ((change.flags & EV_ADD) != 0 || change.data != 0)) { + if (change.filter == EVFILT_TIMER && ((change.flags & EV_ADD) != 0 || change.data != 0 || change.fflags != 0)) { NotImplemented_nid_no_patch("kevent: changing an existing EVFILT_TIMER"); } const bool descriptor = change.filter == EVFILT_READ || change.filter == EVFILT_WRITE; diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index dc8b248779..dc2c629db3 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -1153,6 +1153,24 @@ static void VnodeOneshotWatchStops() { Require(std::filesystem::remove(path)); } +static void TimerDisableEnableRearm() { + const int kq = kqueue_nid_postfix(); + int marker = 0; + KernelEvent events[4]{}; + Require(Apply(kq, Change(1, EvfiltTimer, EvAdd, NoteUseconds, 50000, &marker)) == 0); + Require(Apply(kq, Change(1, EvfiltTimer, EvDisable, 0, 0, &marker)) == 0); + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + Require(Apply(kq, Change(1, EvfiltTimer, EvEnable, 0, 0, &marker)) == 0); + Require(Wait(kq, events, 4) == 1); + Require(events[0].ident == 1 && events[0].filter == EvfiltTimer && events[0].udata == &marker); + Require(events[0].data >= 2 && events[0].data <= 6); + Require(Wait(kq, events, 4) == 1); + Require(events[0].data == 1); + Require(Throws([kq] { Apply(kq, Change(1, EvfiltTimer, EvAdd | EvEnable)); })); + Require(Apply(kq, Change(1, EvfiltTimer, EvDelete)) == 0); + Require(close_nid_postfix(kq) == 0); +} + static void PeriodicTimerFiresTwice() { const int kq = kqueue_nid_postfix(); KernelEvent events[4]{}; @@ -1285,6 +1303,7 @@ int main() { RenameAndDeleteWatchedFile(); VnodeOneshotWatchStops(); PeriodicTimerFiresTwice(); + TimerDisableEnableRearm(); #endif #ifdef _WIN32 RecycleBinDeleteWatchedFile(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index db1c65a38f..78def54738 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -273,4 +273,4 @@ - [sysctl MIBs for kern.smp.maxcpus, kern.stackprot, kern.sched.name and kern.timecounter.hardware](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - on a PS5 (firmware 12.70), a payload process resolves these names to {1, 2147482816, 2147482814}, {1, 2147483151}, {1, 2147483182, 2147483181} and {1, 2147482838, 2147482834}; `sysctl` returns -1 with `ENOENT` for reads and size queries by those exact MIBs, leaving the buffer and length unchanged, including when either or both output pointers are null. Writes were not measured and throw. `sysctlnametomib` still throws for these four names; #7541 implements the mappings for nine of the other names. - [sceKernelGetProcessAbiType](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) throws: no measured value, prototype or documented consumer of the process ABI type was found, and PS5_MihawkOS (`mihawk-99/PS5_MihawkOS`, PPSA99170) is the only title known to import it. Returning a guessed constant would let a title branch on a value the console never answered with, so the declaration only resolves the import until the console value is measured - [flock on a directory descriptor](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) returns 0 on Windows without taking a lock, as FreeBSD 11.4's `sys_flock` accepts any vnode descriptor and so succeeds on a directory; Windows `LockFileEx` rejects a directory handle, which used to abort the process. Two descriptors of the same directory therefore never conflict: a second `LOCK_EX | LOCK_NB` succeeds where FreeBSD fails with `EWOULDBLOCK`. Linux and macOS use the host `flock`. Not measured on a console -- [EVFILT_TIMER modify with data 0](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - a timer change with `EV_ADD` unset and `data == 0` keeps the note's period instead of taking FreeBSD's zero default, which would fire without delay; whether the console keeps or zeroes the period is unverified. +- [EVFILT_TIMER modify with data 0](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - a timer change with `EV_ADD`, `data` and `fflags` unset keeps the note's period instead of taking FreeBSD's zero default, which would fire without delay; whether the console keeps or zeroes the period is unverified. `EV_ENABLE` on a disabled timer reports the expiries it missed while disabled, as FreeBSD 11's `filt_timerexpire` counts a disabled note and keeps its phase: the missed count is summed in one step at the first wait and the next expiry keeps the note's phase, which `TimerDisableEnableRearm` pins; not measured on a console. A change that sets `EV_ADD`, `data` or `fflags` still throws, waiting for the measured re-add rules: FreeBSD 11's `kqueue_register` copies `data` and `fflags` onto the note and the new period applies at the next expiry. From 252374a3098206b4ca37d4754648c982ed237323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 24 Oct 2026 10:22:41 +0200 Subject: [PATCH 190/428] test(libkernel): tolerate a late wake in the disabled timer's second expiry --- core/libs/tests/GuestKqueue.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index dc2c629db3..92e5f89f0c 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -1165,7 +1165,7 @@ static void TimerDisableEnableRearm() { Require(events[0].ident == 1 && events[0].filter == EvfiltTimer && events[0].udata == &marker); Require(events[0].data >= 2 && events[0].data <= 6); Require(Wait(kq, events, 4) == 1); - Require(events[0].data == 1); + Require(events[0].data >= 1 && events[0].data <= 2); Require(Throws([kq] { Apply(kq, Change(1, EvfiltTimer, EvAdd | EvEnable)); })); Require(Apply(kq, Change(1, EvfiltTimer, EvDelete)) == 0); Require(close_nid_postfix(kq) == 0); From 387f069744882ef867fe2179ce73ec371e653824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 24 Oct 2026 10:58:01 +0200 Subject: [PATCH 191/428] chore(libkernel): drop code comments from the kqueue tests and move the timer debt line off the section end Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestKqueue.cpp | 6 ------ docs/dev/debt/libkernel.md | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index 92e5f89f0c..b78ee0782d 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -1136,15 +1136,11 @@ static void VnodeOneshotWatchStops() { Require(write_nid_postfix(file, "abc", 3) == 3); Require(Wait(kq, events, 4) == 1); Require(events[0].ident == ident && events[0].filter == EvfiltVnode && (events[0].flags & EvOneshot) != 0 && events[0].udata == &marker); - // The retired note stopped its watch: further writes produce nothing and the note is gone. Require(write_nid_postfix(file, "def", 3) == 3); std::this_thread::sleep_for(std::chrono::milliseconds(300)); Require(Poll(kq, events, 4) == 0); RequireFailure(Apply(kq, Change(ident, EvfiltVnode, EvDelete)), GuestEnoent); #ifdef __linux__ - // The retired oneshot releases its host watch rather than leaving it running: the inotify - // descriptor count returns to what it was before the watch (each retired oneshot used to - // leak its detached watch thread). for (int attempt = 0; attempt < 100 && InotifyWatchCount() != watchesBefore; ++attempt) std::this_thread::sleep_for(std::chrono::milliseconds(20)); Require(InotifyWatchCount() == watchesBefore); #endif @@ -1183,8 +1179,6 @@ static void PeriodicTimerFiresTwice() { #ifdef _WIN32 static void RecycleBinDeleteWatchedFile() { - // Windows deletes surface as a rename into the POSIX recycle bin; a path under - // \\$Extend\\$Deleted\\ is a delete for the watch. const auto directory = TemporaryName("deleted"); std::filesystem::create_directories(directory + "\\$Extend\\$Deleted"); const auto path = directory + "\\watched"; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 78def54738..efd2093b9a 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -198,6 +198,7 @@ - [Host CPU check](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp) (libkernel) - when libkernel loads on a Linux or Windows x86-64 host, it logs which of the PS5 CPU's user-mode extensions CPUID reports and throws, naming the host CPU, if any extension is missing that LLVM selects from plain C/C++ for the PS5's Zen 2 (`-march=znver2`, [feature list](https://github.com/llvm/llvm-project/blob/llvmorg-19.1.7/llvm/lib/Target/X86/X86.td#L1459-L1513)): SSE to SSE4.2, SSSE3, POPCNT, LZCNT, BMI1, BMI2, MOVBE, CMPXCHG16B, LAHF/SAHF, AVX, AVX2, FMA, F16C, and the operating system's AVX state in XCR0 (#3719). SSE4A, CLZERO and SHA, which `--to-intel` converts, and AES, PCLMULQDQ, RDRAND, RDSEED, ADX, CLFLUSHOPT, CLWB, RDPID, XSAVEOPT, XSAVEC and PREFETCHW, which compilers emit only for intrinsics or explicit prefetches, are logged but not required: a title that executes one of them on a host without it still stops with an illegal instruction. macOS hosts are not checked: some Rosetta versions execute extensions without reporting them through CPUID, but support differs by host. ANDN and RDRAND fault on an M1 with macOS 15.3.1; see the [Rosetta conversion limits](relinker.md) - [machdep.tsc_freq](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the host TSC frequency that `sceKernelGetTscFrequency` measures, as a 64-bit value like FreeBSD 11's [`sysctl_machdep_tsc_freq`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/tsc.c#L658-L676). FreeBSD lets root write it to change the TSC timecounter's frequency; a write here returns -1 with `EPERM`. PPSA99203 reads it at start-up and measures the TSC itself when the value is 100 MHz or less - [getrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `RLIMIT_CPU`, `RLIMIT_FSIZE`, `RLIMIT_CORE`, `RLIMIT_SBSIZE`, `RLIMIT_VMEM`/`AS`, `RLIMIT_NPTS`, `RLIMIT_SWAP`, `RLIMIT_KQUEUES` and `RLIMIT_UMTXP` report infinity as in FreeBSD's process-0 defaults, and `RLIMIT_DATA` mirrors the host limit; a payload process on a PS5 (firmware 12.70) reads 0x800000000 for both limits. `RLIMIT_NOFILE` reports, as both soft and hard limit, the number of descriptors after which AnyPS5 itself fails with `EMFILE`: on Windows the C runtime's 8192 low-level descriptors, which guest files, pipes and sockets all take, and as many stdio streams, which libkernel raises from 512 at load with [`_setmaxstdio`](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/setmaxstdio), on Linux the host's `RLIMIT_NOFILE`, whose soft limit libkernel raises to the hard limit at load, and on macOS the smaller of the host's hard limit and `kern.maxfilesperproc`, which libkernel sets as both limits at load, since macOS fails with `EMFILE` at `kern.maxfilesperproc` whatever the soft limit. A payload process on a PS5 (firmware 12.70) reads 13952 for both limits, while `getdtablesize` returns 12555 and the process can open descriptors up to 12554. XPSemu (PPSA97358) reads `RLIMIT_NOFILE` in QEMU's `os_setup_limits` and raises the soft limit to the hard one when they differ. `RLIMIT_STACK` (0x200000), `RLIMIT_RSS` and `RLIMIT_MEMLOCK` (infinity) and `RLIMIT_NPROC` (1604) report the soft and hard limits a payload process reads on a PS5 (firmware 12.70). Guest locking still follows the host: `mlock` and `sceKernelMlock` fail at the host's hard `RLIMIT_MEMLOCK` on Linux, and on Windows when the process working set cannot grow, although `RLIMIT_MEMLOCK` reports infinity. There the main thread's stack is 0x200000 as well (`pthread_attr_get_np`); here the main thread runs on the host thread's stack, whose size can differ. Whether a title process reads the same limits is unverified. +- [EVFILT_TIMER modify with data 0](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - a timer change with `EV_ADD`, `data` and `fflags` unset keeps the note's period instead of taking FreeBSD's zero default, which would fire without delay; whether the console keeps or zeroes the period is unverified. `EV_ENABLE` on a disabled timer reports the expiries it missed while disabled, as FreeBSD 11's `filt_timerexpire` counts a disabled note and keeps its phase: the missed count is summed in one step at the first wait and the next expiry keeps the note's phase, which `TimerDisableEnableRearm` pins; not measured on a console. A change that sets `EV_ADD`, `data` or `fflags` still throws, waiting for the measured re-add rules: FreeBSD 11's `kqueue_register` copies `data` and `fflags` onto the note and the new period applies at the next expiry. - [setrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) validates the resource and input in FreeBSD order, maps negative limits to infinity, and lowers a soft limit above the hard limit. A request other than `RLIMIT_NPROC` succeeds when its normalized values match `getrlimit`, or, for `RLIMIT_NOFILE`, when `rlim_max` does not exceed the current hard limit and `rlim_cur` does not exceed `rlim_max`, storing the updated limits for subsequent `getrlimit` queries; descriptor allocation enforcement of the lowered soft limit is not implemented, and changing other limits throws because guest limit state is not stored. In a payload process on a PS5 (firmware 12.70), `setrlimit(RLIMIT_NOFILE)` lowers a requested 13952 to 12555, as the hard and as the soft limit; here a request within the current hard limit is stored as given and one above it throws; setting `RLIMIT_STACK`, `RLIMIT_RSS` and `RLIMIT_MEMLOCK` to their current values succeeds and changes nothing, as here; setting `RLIMIT_NPROC` to its current 1604 also succeeds but leaves 1443. `RLIMIT_NPROC` follows FreeBSD 11 by clamping both values to 1 through `maxprocperuid`, stores the result and returns it from `getrlimit`. The initial 1604 and `maxprocperuid` 1443 follow that measurement: process 0's limit starts at `maxproc` ([`init_main.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/init_main.c#L550-L551)), `maxprocperuid` starts at nine tenths of it ([`subr_param.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/subr_param.c#L258)), and `setrlimit` applies that bound ([`kern_resource.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_resource.c#L714-L718)). `kern.maxproc` fails with `ENOENT` in the payload process, so the values cannot be queried independently, and process creation does not enforce the stored limit. Java 25 LAN / DNS (PPSA99869) sets `rlim_cur = MIN(0x100000, rlim_max)` during VM initialization - [execvp](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported. PPSA85300 imports it and fails to load without it; its `fork` and `vfork` fail with `ENOSYS` ([libc_missing.c](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/ps5/compat/libc_missing.c#L290-L292)), so the child process that would call it is never created. - [nmount](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throws: mounting file systems is not supported. Prospero Themes (PPSA90210) imports it and fails to load without it; it opens its theme library without calling it, and its strings (`/dev/ssd0.system_ex`, `/system_ex`, `exfatfs`, `fstype`, `fspath`, `from`, `ignoreacl`) point to remounting the console's `/system_ex` partition to write theme files there. @@ -273,4 +274,3 @@ - [sysctl MIBs for kern.smp.maxcpus, kern.stackprot, kern.sched.name and kern.timecounter.hardware](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - on a PS5 (firmware 12.70), a payload process resolves these names to {1, 2147482816, 2147482814}, {1, 2147483151}, {1, 2147483182, 2147483181} and {1, 2147482838, 2147482834}; `sysctl` returns -1 with `ENOENT` for reads and size queries by those exact MIBs, leaving the buffer and length unchanged, including when either or both output pointers are null. Writes were not measured and throw. `sysctlnametomib` still throws for these four names; #7541 implements the mappings for nine of the other names. - [sceKernelGetProcessAbiType](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) throws: no measured value, prototype or documented consumer of the process ABI type was found, and PS5_MihawkOS (`mihawk-99/PS5_MihawkOS`, PPSA99170) is the only title known to import it. Returning a guessed constant would let a title branch on a value the console never answered with, so the declaration only resolves the import until the console value is measured - [flock on a directory descriptor](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) returns 0 on Windows without taking a lock, as FreeBSD 11.4's `sys_flock` accepts any vnode descriptor and so succeeds on a directory; Windows `LockFileEx` rejects a directory handle, which used to abort the process. Two descriptors of the same directory therefore never conflict: a second `LOCK_EX | LOCK_NB` succeeds where FreeBSD fails with `EWOULDBLOCK`. Linux and macOS use the host `flock`. Not measured on a console -- [EVFILT_TIMER modify with data 0](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - a timer change with `EV_ADD`, `data` and `fflags` unset keeps the note's period instead of taking FreeBSD's zero default, which would fire without delay; whether the console keeps or zeroes the period is unverified. `EV_ENABLE` on a disabled timer reports the expiries it missed while disabled, as FreeBSD 11's `filt_timerexpire` counts a disabled note and keeps its phase: the missed count is summed in one step at the first wait and the next expiry keeps the note's phase, which `TimerDisableEnableRearm` pins; not measured on a console. A change that sets `EV_ADD`, `data` or `fflags` still throws, waiting for the measured re-add rules: FreeBSD 11's `kqueue_register` copies `data` and `fflags` onto the note and the new period applies at the next expiry. From 0d6a6f96eeee90b4998deaadae50ef5e3cf01a53 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Sat, 24 Oct 2026 04:28:41 -0400 Subject: [PATCH 192/428] fix(agc): reclaim idle device buffers up to the storage cache's current cap Co-Authored-By: Claude Opus 5.5 --- .../Graphics/include/ShaderResources.hpp | 1 + .../Graphics/src/ShaderResources.cpp | 14 +++++++------ .../prx/libSceAgcDriver/tests/Graphics.cpp | 21 +++++++++++++++++++ 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index b3d8776df4..e22e2c7864 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -32,6 +32,7 @@ void FlushCachedTextures(VkDevice device); void ClearCachedTextures(VkDevice device); std::uint64_t ReleaseTextureCaches(std::uint64_t bytes, bool newImage = false); std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory); +std::uint64_t StorageTextureCeiling(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported); std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t sampledBytes, std::uint64_t storageBytes); std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes, std::uint64_t storageBytes = 0); bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::chrono::steady_clock::duration sinceReport); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 9833032de0..ab3ffc086f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -624,10 +624,7 @@ std::uint64_t storageBudget(const Context& context, std::uint64_t required = 0) cached.device = context.physical; cached.read = now; cached.budget = StorageTextureBudget(context.memory, &reported, SampledTextureMemory(), storageCacheBytes()); - cached.ceiling = cached.budget; - if (const auto heap = largestDeviceLocalHeap(context.memory); heap && reported.heapBudget[*heap] != 0) { - cached.ceiling = std::max(512ull << 20u, std::min(TextureCacheBudget(context.memory), reported.heapBudget[*heap] / 4u)); - } + cached.ceiling = StorageTextureCeiling(context.memory, &reported); }; if (cached.device != context.physical || now - cached.read >= std::chrono::seconds(1)) read(); const auto needed = std::min(required, cached.ceiling); @@ -1283,14 +1280,19 @@ std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory) return std::max(2048ull << 20u, heap ? memory.memoryHeaps[*heap].size / 4u : 0u); } +std::uint64_t StorageTextureCeiling(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported) { + const auto heap = largestDeviceLocalHeap(memory); + if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory); + return std::max(512ull << 20u, std::min(memory.memoryHeaps[*heap].size / 2u, reported->heapBudget[*heap] / 2u)); +} + std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t sampledBytes, std::uint64_t storageBytes) { const auto heap = largestDeviceLocalHeap(memory); if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory); const auto budget = reported->heapBudget[*heap]; const auto usage = reported->heapUsage[*heap]; const auto reserved = usage - std::min(usage, sampledBytes + storageBytes) + sampledBytes + budget / 8u; - const auto cap = std::min(memory.memoryHeaps[*heap].size / 2u, budget / 2u); - return std::max(512ull << 20u, std::min(cap, budget > reserved ? budget - reserved : 0u)); + return std::min(StorageTextureCeiling(memory, reported), std::max(512ull << 20u, budget > reserved ? budget - reserved : 0u)); } std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes, std::uint64_t storageBytes) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index ffa2c44a13..2411ed17f3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -7025,6 +7025,27 @@ void storageTextureBudgetTests() { memory.memoryHeaps[0].flags = 0; memory.memoryHeaps[2].flags = 0; Require(StorageTextureBudget(memory, &reported, 0, 0) == 2 * GiB, "a device without a device-local heap does not keep 2 GiB of storage images"); + using AgcDriver::Graphics::StorageTextureCeiling; + memory.memoryHeaps[0] = {256 * MiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + memory.memoryHeaps[2] = {16 * GiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + reported = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + Require(StorageTextureCeiling(memory, nullptr) == 4 * GiB, "without VK_EXT_memory_budget the storage image cache ceiling is not a quarter of the 16 GiB heap"); + reported.heapBudget[2] = 6 * GiB; + Require(StorageTextureCeiling(memory, &reported) == 3 * GiB, "the storage image cache ceiling on a 6 GiB budget is not half of the budget, the cap the cache may reach"); + reported.heapBudget[2] = 40 * GiB; + Require(StorageTextureCeiling(memory, &reported) == 8 * GiB, "the storage image cache ceiling on a 40 GiB budget is not half of the 16 GiB heap"); + reported.heapBudget[2] = 768 * MiB; + Require(StorageTextureCeiling(memory, &reported) == 512 * MiB, "the storage image cache ceiling on a 768 MiB budget does not keep the 512 MiB floor"); + for (std::uint64_t budget = 1 * GiB; budget <= 24 * GiB; budget += 512 * MiB) { + reported.heapBudget[2] = budget; + const auto ceiling = StorageTextureCeiling(memory, &reported); + for (std::uint64_t used = 0; used <= budget; used += 256 * MiB) { + reported.heapUsage[2] = used; + Require(StorageTextureBudget(memory, &reported, 256 * MiB, 512 * MiB) <= ceiling, "the storage image cache budget exceeds the ceiling idle device buffers are reclaimed up to"); + } + reported.heapUsage[2] = 0; + Require(StorageTextureBudget(memory, &reported, 0, 0) == ceiling, "an idle heap does not give the storage image cache its full ceiling"); + } } void sampledBudgetReportTests() { From 8ee49632b059b7c98e57a0f9155f133f0c85bc85 Mon Sep 17 00:00:00 2001 From: xTRACKGOD <135815363+xTRACKGOD@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:17:58 +0530 Subject: [PATCH 193/428] fix(tools): detect host and title changes in interval digests --- docs/dev/BUILD.md | 2 +- tools/tests/test_trace_digest.py | 66 +++++++++++++++++++++++++++++++- tools/trace_digest.py | 5 ++- 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 8384d87e1f..cbe6d48000 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -246,7 +246,7 @@ The switch combines with `APS5_TRACE_FRAMES` or `APS5_TRACE_SECONDS`. The interv Set `APS5_TRACE_BENCH=1` together with `APS5_TRACE_SUMMARY_EVERY` to also keep every interval's frame times for a benchmark report. At each interval the presenter hands the kept intervals to a writer thread, which formats and compresses the file and rewrites `aps5-bench--.json.gz` in `APS5_TRACE_DIR` or the working directory: one JSON object with `format` (`aps5-bench/1`), the identity fields of the summary line (`commit`, `dirty`, `env`, `gpu`, `driver`, `os`, `cpu`, `cpu_threads`, `memory_mib` and, with `param.json`, `title_id`, `title` and `content_version`), `intervals_dropped` and `intervals`. Each interval has `start_s`, `end_s`, `first_present`, `resolution`, `gpu_busy_ms` (the interval's total) and `frame_us`, the whole microseconds between consecutive presents. The file keeps the last 240 intervals; `intervals_dropped` counts older ones it no longer holds. Any other value, or the switch without `APS5_TRACE_SUMMARY_EVERY`, fails the first present. The file is written to `.tmp` and then moved over the previous one, so a crash or kill during a rewrite leaves the last complete report. Only the newest report waits for the writer: one handed over while an older one is still pending replaces it. The frame times cost 4 bytes per present in memory. -`python tools/trace_digest.py aps5-summary.jsonl` prints the interval lines of such a file as one row per interval (frame p50, p90, p99, max, GPU busy per present, the busiest queue worker and the largest phase) and a text sparkline of the frame p99 and of the GPU busy time; lines of window captures in the file are skipped, and a change of commit, environment, GPU, driver or OS between intervals is reported. `--json` prints the rows as JSON. +`python tools/trace_digest.py aps5-summary.jsonl` prints the interval lines of such a file as one row per interval (frame p50, p90, p99, max, GPU busy per present, the busiest queue worker and the largest phase) and a text sparkline of the frame p99 and of the GPU busy time; lines of window captures in the file are skipped, and changes to build, environment, host, resolution or title metadata between intervals are reported. `--json` prints the rows, the first interval's identity and the indices of identity changes as JSON. With `APS5_TRACE_SUMMARY_EVERY` set, the recorder writes a timestamp pair per batch for the whole run and every `PerformanceTimer` scope adds its time to the thread's totals, two clock reads per scope. A thread's totals are read while it runs, so a scope that straddles an interval boundary is counted in one interval or the other. diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index 8099ca9eee..9e5f26c1bc 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -652,7 +652,71 @@ class IntervalDigestTests(unittest.TestCase): for index, commit in enumerate(("a", "a", "b", "b", "a"))] result = trace_digest.digest_intervals(lines) self.assertEqual(result["identity_changed_at"], [2, 4]) - self.assertIn("commit, environment, GPU, driver or OS changed at interval 3, 5", trace_digest.markdown_intervals(result).splitlines()) + self.assertIn("build, environment, host, resolution or title metadata changed at interval 3, 5", trace_digest.markdown_intervals(result).splitlines()) + + def test_machine_and_title_identity_fields_are_retained_and_compared(self): + metadata = {"cpu": "CPU A", "cpu_threads": 16, "memory_mib": 32768, + "title_id": "PPSA00001", "title": "Title One", "content_version": "01.000.000"} + first = interval_line(0.0, 30.0, 20.0, 5.0, **metadata) + for key, value in (("cpu", "CPU B"), ("cpu_threads", 32), ("memory_mib", 65536), + ("title_id", "PPSA00002"), ("title", "Title Two"), ("content_version", "02.000.000")): + with self.subTest(key=key): + changed = dict(first, **{key: value}) + result = trace_digest.digest_intervals([first, first, changed, changed, first]) + self.assertEqual({key: result["identity"].get(key) for key in metadata}, metadata) + self.assertEqual(result["identity_changed_at"], [2, 4]) + self.assertIn("changed at interval 3, 5", trace_digest.markdown_intervals(result)) + + def test_optional_identity_appearance_disappearance_and_return(self): + absent = interval_line(0.0, 30.0, 20.0, 5.0) + for key, value in (("cpu", "CPU A"), ("cpu_threads", 0), ("memory_mib", 0), + ("title_id", "PPSA00001"), ("title", "Title One"), ("content_version", "")): + with self.subTest(key=key, value=value): + present = dict(absent, **{key: value}) + result = trace_digest.digest_intervals([absent, present, present, absent, present]) + self.assertEqual(result["identity_changed_at"], [1, 3, 4]) + self.assertIn(key, result["identity"]) + self.assertIsNone(result["identity"][key]) + self.assertIn("changed at interval 2, 4, 5", trace_digest.markdown_intervals(result)) + + def test_stable_or_absent_optional_identity_does_not_report_changes(self): + absent = interval_line(0.0, 30.0, 20.0, 5.0) + metadata = {"cpu": "CPU A", "cpu_threads": 0, "memory_mib": 0, + "title_id": "PPSA00001", "title": "Title One", "content_version": ""} + for line in (absent, dict(absent, **metadata)): + with self.subTest(line=line): + result = trace_digest.digest_intervals([line, line]) + self.assertEqual(result["identity_changed_at"], []) + self.assertNotIn("changed at interval", trace_digest.markdown_intervals(result)) + + def test_resolution_changes_are_reported(self): + first = interval_line(0.0, 30.0, 20.0, 5.0) + result = trace_digest.digest_intervals([first, dict(first, resolution="1280x720")]) + self.assertEqual(result["identity_changed_at"], [1]) + self.assertIn("build, environment, host, resolution or title metadata changed at interval 2", + trace_digest.markdown_intervals(result)) + + def test_cli_retains_machine_and_title_identity_and_reports_changes(self): + metadata = {"cpu": "CPU A", "cpu_threads": 16, "memory_mib": 32768, + "title_id": "PPSA00001", "title": "Title One", "content_version": "01.000.000"} + first = interval_line(0.0, 30.0, 20.0, 5.0, **metadata) + second = interval_line(30.0, 60.0, 40.0, 10.0, **dict(metadata, content_version="02.000.000")) + self.path.write_text("\n".join(json.dumps(line) for line in (first, second)) + "\n", encoding="utf-8") + result = subprocess.run([sys.executable, trace_digest.__file__, str(self.path), "--json"], + capture_output=True, text=True, encoding="utf-8", timeout=20) + self.assertEqual(result.returncode, 0, result.stderr) + report = json.loads(result.stdout) + self.assertEqual({key: report["identity"].get(key) for key in metadata}, metadata) + self.assertEqual(report["identity_changed_at"], [1]) + without_metadata = [{key: value for key, value in line.items() if key not in metadata} for line in (first, second)] + unchanged_metrics = trace_digest.digest_intervals(without_metadata) + self.assertEqual(report["intervals"], unchanged_metrics["intervals"]) + result = subprocess.run([sys.executable, trace_digest.__file__, str(self.path)], + capture_output=True, text=True, encoding="utf-8", timeout=20) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("build, environment, host, resolution or title metadata changed at interval 2", result.stdout) + expected = trace_digest.markdown_intervals(unchanged_metrics) + self.assertEqual(result.stdout[result.stdout.index("| # |"):], expected[expected.index("| # |"):]) def test_sparkline_scales_to_the_largest_value(self): self.assertEqual(trace_digest.sparkline([0.0, 5.0, 10.0]), ".+@") diff --git a/tools/trace_digest.py b/tools/trace_digest.py index 59c9fb3bf9..7998b670bb 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -308,7 +308,8 @@ def digest_intervals(lines): "top_phase": {"name": largest[0], "ms_per_frame": largest[1]} if largest else None, "trace_dropped": line.get("trace_dropped", False)}) first = lines[0] if lines else {} - keys = ("commit", "dirty", "env", "gpu", "driver", "os", "resolution") + keys = ("commit", "dirty", "env", "gpu", "driver", "os", "resolution", + "cpu", "cpu_threads", "memory_mib", "title_id", "title", "content_version") changed = [index for index in range(1, len(lines)) if any(lines[index].get(key) != lines[index - 1].get(key) for key in keys)] return {"intervals": rows, "identity": {key: first.get(key) for key in keys}, "identity_changed_at": changed} @@ -322,7 +323,7 @@ def markdown_intervals(result): f"{identity['gpu']}, {identity['driver']}, {identity['os']}, {identity['resolution']}, commit {identity['commit']}" f"{' (dirty)' if identity.get('dirty') else ''}"] if result["identity_changed_at"]: - lines.append("commit, environment, GPU, driver or OS changed at interval " + ", ".join(str(index + 1) for index in result["identity_changed_at"])) + lines.append("build, environment, host, resolution or title metadata changed at interval " + ", ".join(str(index + 1) for index in result["identity_changed_at"])) lines += ["", "| # | seconds | presents | p50 | p90 | p99 | max | GPU busy | busiest worker | largest phase |", "|---|---|---|---|---|---|---|---|---|---|"] for index, row in enumerate(rows, 1): frame = row["frame_ms"] From 7f4d4aff8966a88633477d5bf18424b233b1bdcb Mon Sep 17 00:00:00 2001 From: krishalivaibhav Date: Sat, 24 Oct 2026 15:21:49 +0530 Subject: [PATCH 194/428] fix(libSceLibreSsl): build the AES substitution table within the constexpr step limit The table searched all 255 candidates for each inverse, which exceeds the constexpr step limit of AppleClang. The inverse is now computed as x^254 in GF(2^8), which gives the same table. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceLibreSsl/src/Aes.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceLibreSsl/src/Aes.cpp b/core/libs/prx/libSceLibreSsl/src/Aes.cpp index 3009da6ec1..6360e78287 100644 --- a/core/libs/prx/libSceLibreSsl/src/Aes.cpp +++ b/core/libs/prx/libSceLibreSsl/src/Aes.cpp @@ -31,9 +31,10 @@ constexpr std::uint8_t Multiply(std::uint8_t a, std::uint8_t b) { constexpr std::array MakeSubstitution() { std::array box {}; for (int value = 0; value < 256; ++value) { - std::uint8_t inverse = 0; - for (int candidate = 1; candidate < 256 && value; ++candidate) - if (Multiply(static_cast(value), static_cast(candidate)) == 1) inverse = static_cast(candidate); + const auto element = static_cast(value); + std::uint8_t inverse = element; + for (int step = 0; step < 6; ++step) inverse = Multiply(Multiply(inverse, inverse), element); + inverse = Multiply(inverse, inverse); std::uint8_t result = 0x63; for (int shift = 0; shift < 5; ++shift) result ^= static_cast((inverse << shift) | (inverse >> ((8 - shift) & 7))); From e5697e7f5582f0854a3511474359207e9f55333d Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Sat, 24 Oct 2026 09:51:35 +0000 Subject: [PATCH 195/428] fix(relinker): reject overlapping PT_LOAD segments in Linux output The Linux patcher sorted the PT_LOAD segments by address but never checked that their memory ranges are disjoint, while the Windows and macOS images and guest modules reject overlapping loads. The Linux output kept both headers, the kernel mapped the later segment over the earlier one, and the program crashed at start. Adjacent loads whose ranges overlap now fail with "Overlapping PT_LOAD memory ranges", the message the other targets use. --- .../elfpatcher/src/linux/LinuxElfPatcher.cpp | 5 +++++ .../relinker/tests/test_executable_segments.py | 14 +++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index b2f84ae52d..a52776eace 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -113,6 +113,11 @@ void SeparateSharedLoadPages(std::vector& buf, std::vector Date: Sat, 24 Oct 2026 22:58:58 +1300 Subject: [PATCH 196/428] feat(libSceVrTracker2): validate initialization and pass through the Hmd2 tracking result sceVrTracker2Initialize follows the order read from firmware 13.60 libSceVrTracker2.sprx (disassembly, not measured on a console). It rejects with 0x8A740006, in order: a null record or a size other than 72; a mode other than 0, 1000 or 1001; a null work memory; a size below the mode's 32 MiB, 7 MiB or 3 MiB; an alignment below 0x10000; a non-zero reserved byte at 0x3a..0x47; a thread priority outside 256..767; an affinity mask outside bits 0..12 in mode 0 or outside 0xEA00 in modes 1000 and 1001. It then moves the module state from 0 to 1 and returns 0x8A740002 if it was not 0. From there every failure undoes what the call opened and resets the state to 0. Mode 0 clears the first 32 MiB of work memory first. The application attribute checks on the sceKernelGetAppInfo2 record (0x8A74001E, 0x8A740020) are skipped: the host has none, and a zeroed record passes them. The signed option at 0x39 accepts -1 and 0, and 1 only for titles compiled with SDK 7.00 or later. Modes 0 and 1000 then call libSceHmd2 TrackingOpen, TrackingStart, LedDetectorOpen and LedDetectorStart and return the first non-zero result unchanged. Without a headset main's libSceHmd2 answers 0 and -1 (its measured pre-initialization answers), so Initialize returns -1 and every other export keeps answering 0x8A740001. Modes 1000 and 1001 need the application category, which the host does not have, and a fully successful Hmd2 sequence would need the tracker runtime; both throw. The eight tracking and LED-detector exports become named functions in libSceHmd2 so that libSceVrTracker2 can import them by name; their answers are unchanged. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceHmd2/Export.cpp | 40 ++++- core/libs/prx/libSceVrTracker2/CMakeLists.txt | 4 +- core/libs/prx/libSceVrTracker2/Export.cpp | 136 +++++++++++++++- core/libs/tests/GuestVrTracker2.cpp | 151 +++++++++++++++++- docs/dev/debt/libSceVrTracker2.md | 2 +- 5 files changed, 312 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index d744bf2741..15aa711e1b 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -520,10 +520,6 @@ int APS5_VABI sceHmd2SetIpdOffset(std::uint8_t enabled, float offset) { } APS5_EXPORT("-kZT7X7OMg0", Hmd2BeforeInitializeZero); APS5_EXPORT("1ewLtng-SVk", Hmd2BeforeInitializeZero); -APS5_EXPORT("B0b8DeIUGes", Hmd2BeforeInitializeZero); -APS5_EXPORT("HclLg4Oj5yw", Hmd2BeforeInitializeZero); -APS5_EXPORT("VbEvrQXsZyY", Hmd2BeforeInitializeZero); -APS5_EXPORT("bB6MKZi5UCQ", Hmd2BeforeInitializeZero); APS5_EXPORT("OXQ1EDf02XU", Hmd2BeforeInitializeZero); APS5_EXPORT("IAxfMTvFm0M", Hmd2BeforeInitializeZero); APS5_EXPORT("qTJtAtaLeno", Hmd2BeforeInitializeZero); @@ -680,10 +676,6 @@ std::int32_t APS5_VABI Hmd2BeforeInitializeMinusOne() { } APS5_EXPORT("EAkv8Mqt33E", Hmd2BeforeInitializeMinusOne); APS5_EXPORT("Rv7bkmKvmz0", Hmd2BeforeInitializeMinusOne); -APS5_EXPORT("1oPZizn0SNA", Hmd2BeforeInitializeMinusOne); -APS5_EXPORT("TwCpE-nNrlo", Hmd2BeforeInitializeMinusOne); -APS5_EXPORT("jkqAeQfdtxw", Hmd2BeforeInitializeMinusOne); -APS5_EXPORT("+l0RQph81Z8", Hmd2BeforeInitializeMinusOne); APS5_EXPORT("qsmAtGw1Ncc", Hmd2BeforeInitializeMinusOne); APS5_EXPORT("uKhwYoMsIsA", Hmd2BeforeInitializeMinusOne); APS5_EXPORT("WNdaigr94dI", Hmd2BeforeInitializeMinusOne); @@ -691,6 +683,38 @@ APS5_EXPORT("P6S5ZVWf-H0", Hmd2BeforeInitializeMinusOne); APS5_EXPORT("sun6NWE1sdg", Hmd2BeforeInitializeMinusOne); APS5_EXPORT("--TFsJYXo04", Hmd2BeforeInitializeMinusOne); +std::int32_t APS5_VABI sceHmd2InternalTrackingOpen() { + return BeforeInitialize(0, __func__); +} + +std::int32_t APS5_VABI sceHmd2InternalTrackingClose() { + return BeforeInitialize(0, __func__); +} + +std::int32_t APS5_VABI sceHmd2InternalLedDetectorOpen() { + return BeforeInitialize(0, __func__); +} + +std::int32_t APS5_VABI sceHmd2InternalLedDetectorClose() { + return BeforeInitialize(0, __func__); +} + +std::int32_t APS5_VABI sceHmd2InternalTrackingStart() { + return BeforeInitialize(-1, __func__); +} + +std::int32_t APS5_VABI sceHmd2InternalTrackingStop() { + return BeforeInitialize(-1, __func__); +} + +std::int32_t APS5_VABI sceHmd2InternalLedDetectorStart() { + return BeforeInitialize(-1, __func__); +} + +std::int32_t APS5_VABI sceHmd2InternalLedDetectorStop() { + return BeforeInitialize(-1, __func__); +} + std::int32_t APS5_VABI Hmd2BeforeInitializeValue0400() { return BeforeInitialize(0x400, __func__); } diff --git a/core/libs/prx/libSceVrTracker2/CMakeLists.txt b/core/libs/prx/libSceVrTracker2/CMakeLists.txt index aaf8e867d1..cccd8e8455 100644 --- a/core/libs/prx/libSceVrTracker2/CMakeLists.txt +++ b/core/libs/prx/libSceVrTracker2/CMakeLists.txt @@ -1,12 +1,12 @@ add_library(libSceVrTracker2 SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceVrTracker2 PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(libSceVrTracker2 PRIVATE libc) +target_link_libraries(libSceVrTracker2 PRIVATE libSceHmd2 libc) configure_windows_unwind(libSceVrTracker2) if(BUILD_TESTING) add_test_executable(guest_vr_tracker2_tests ${LIBS_INCLUDE_DIR}/tests/GuestVrTracker2.cpp) target_include_directories(guest_vr_tracker2_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_vr_tracker2_tests PRIVATE libSceVrTracker2 libc) + target_link_libraries(guest_vr_tracker2_tests PRIVATE libSceVrTracker2 libkernel libc) configure_windows_unwind(guest_vr_tracker2_tests) add_test(NAME guest_vr_tracker2 COMMAND guest_vr_tracker2_tests) if(WIN32) diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index 2e521f452e..ed85cc5331 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -126,12 +127,111 @@ static_assert(sizeof(VrTracker2OrientedBoundingBox) == 80); static_assert(offsetof(VrTracker2OrientedBoundingBox, transform) == 4); static_assert(offsetof(VrTracker2OrientedBoundingBox, extents) == 68); +extern "C" { +std::int32_t APS5_VABI sceHmd2InternalTrackingOpen(); +std::int32_t APS5_VABI sceHmd2InternalTrackingStart(); +std::int32_t APS5_VABI sceHmd2InternalTrackingStop(); +std::int32_t APS5_VABI sceHmd2InternalTrackingClose(); +std::int32_t APS5_VABI sceHmd2InternalLedDetectorOpen(); +std::int32_t APS5_VABI sceHmd2InternalLedDetectorStart(); +std::int32_t APS5_VABI sceHmd2InternalLedDetectorStop(); +std::int32_t APS5_VABI sceHmd2InternalLedDetectorClose(); +} + namespace { constexpr std::int32_t SCE_OK = 0; constexpr std::int32_t VR_TRACKER2_ERROR_NOT_INITIALIZED = static_cast(0x8A740001u); +constexpr std::int32_t VR_TRACKER2_ERROR_ALREADY_INITIALIZED = static_cast(0x8A740002u); constexpr std::int32_t VR_TRACKER2_ERROR_INVALID_ARGUMENT = static_cast(0x8A740006u); +constexpr std::uint32_t TrackerMode = 0; +constexpr std::uint32_t SystemTrackingMode = 1000; +constexpr std::uint32_t SystemClientMode = 1001; +constexpr std::uint32_t MinimumWorkMemoryAlignment = 0x10000; +constexpr std::int32_t MinimumThreadPriority = 256; +constexpr std::int32_t MaximumThreadPriority = 767; +constexpr std::uint64_t TrackerAffinityLimit = 0x2000; +constexpr std::uint64_t SystemAffinityMask = 0xEA00; +constexpr std::uint32_t OptionModeSdkVersion = 0x7000000; + +std::atomic state{0}; + +std::uint32_t WorkMemorySize(std::uint32_t mode) { + if (mode == SystemTrackingMode) return 7u << 20; + if (mode == SystemClientMode) return 3u << 20; + return 32u << 20; +} + +std::uint32_t CompiledSdkVersion() { + const auto* process = static_cast(ApplicationProcessParameters_nid_no_patch()); + std::uint32_t sdkVersion; + std::memcpy(&sdkVersion, process + 0x10, sizeof(sdkVersion)); + return sdkVersion; +} + +std::int32_t ValidateMode(std::uint32_t mode) { + if (mode == SystemTrackingMode || mode == SystemClientMode) NotImplemented_nid_no_patch("sceVrTracker2Initialize: application category of a system mode"); + return mode == TrackerMode ? SCE_OK : VR_TRACKER2_ERROR_INVALID_ARGUMENT; +} + +bool PriorityAllowed(std::int32_t priority) { + return priority >= MinimumThreadPriority && priority <= MaximumThreadPriority; +} + +bool AffinityAllowed(std::uint32_t mode, std::uint64_t mask) { + if (mode == TrackerMode) return mask < TrackerAffinityLimit; + return (mask & ~SystemAffinityMask) == 0; +} + +bool OptionModeAllowed(std::int8_t optionMode) { + if (optionMode == -1 || optionMode == 0) return true; + return optionMode == 1 && CompiledSdkVersion() >= OptionModeSdkVersion; +} + +class InitializeSession { +public: + std::int32_t OpenTracking() { + return track(sceHmd2InternalTrackingOpen(), trackingOpened); + } + + std::int32_t StartTracking() { + return track(sceHmd2InternalTrackingStart(), trackingStarted); + } + + std::int32_t OpenLedDetector() { + return track(sceHmd2InternalLedDetectorOpen(), ledDetectorOpened); + } + + std::int32_t StartLedDetector() { + return track(sceHmd2InternalLedDetectorStart(), ledDetectorStarted); + } + + void Rollback() { + if (ledDetectorStarted) sceHmd2InternalLedDetectorStop(); + if (ledDetectorOpened) sceHmd2InternalLedDetectorClose(); + if (trackingStarted) sceHmd2InternalTrackingStop(); + if (trackingOpened) sceHmd2InternalTrackingClose(); + state.store(0); + } + + std::int32_t Abort(std::int32_t result) { + Rollback(); + return result; + } + +private: + static std::int32_t track(std::int32_t result, bool& done) { + done = result == SCE_OK; + return result; + } + + bool trackingOpened = false; + bool trackingStarted = false; + bool ledDetectorOpened = false; + bool ledDetectorStarted = false; +}; + std::int32_t ClearThenReportNotInitialized(void* output, std::size_t size) { if (output == nullptr) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; std::memset(output, 0, size); @@ -309,16 +409,12 @@ std::int32_t APS5_VABI sceVrTracker2UnregisterDevice(std::int32_t deviceHandle) std::int32_t APS5_VABI sceVrTracker2QueryMemory(const VrTracker2MemoryParameter* parameter, VrTracker2MemoryRequirement* requirement) { if (parameter == nullptr || requirement == nullptr) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; if (parameter->size != sizeof(VrTracker2MemoryParameter)) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; - std::uint32_t memorySize = 32u << 20; - if (parameter->mode == 1000 || parameter->mode == 1001) { - const auto* process = static_cast(ApplicationProcessParameters_nid_no_patch()); - std::uint32_t sdkVersion; - std::memcpy(&sdkVersion, process + 0x10, sizeof(sdkVersion)); - if (sdkVersion < 0x1000000) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; - memorySize = parameter->mode == 1000 ? 7u << 20 : 3u << 20; - } else if (parameter->mode != 0) { + if (parameter->mode == SystemTrackingMode || parameter->mode == SystemClientMode) { + if (CompiledSdkVersion() < 0x1000000) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + } else if (parameter->mode != TrackerMode) { return VR_TRACKER2_ERROR_INVALID_ARGUMENT; } + const std::uint32_t memorySize = WorkMemorySize(parameter->mode); for (const auto value : parameter->reserved) if (value != 0) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; *requirement = {sizeof(VrTracker2MemoryRequirement), memorySize, 64u << 10, {}}; @@ -326,6 +422,30 @@ std::int32_t APS5_VABI sceVrTracker2QueryMemory(const VrTracker2MemoryParameter* } std::int32_t APS5_VABI sceVrTracker2Initialize(const VrTracker2InitializeParameter* parameter) { + if (parameter == nullptr || parameter->size != sizeof(VrTracker2InitializeParameter)) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + const std::uint32_t mode = parameter->mode; + if (const std::int32_t result = ValidateMode(mode); result != SCE_OK) return result; + if (parameter->workMemory == nullptr) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + if (parameter->workMemorySize < WorkMemorySize(mode)) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + if (parameter->workMemoryAlignment < MinimumWorkMemoryAlignment) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + for (const auto value : parameter->reserved) + if (value != 0) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + for (const auto priority : parameter->threadPriorities) + if (!PriorityAllowed(priority)) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + for (const auto mask : parameter->threadAffinityMasks) + if (!AffinityAllowed(mode, mask)) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + std::uint32_t expected = 0; + if (!state.compare_exchange_strong(expected, 1)) return VR_TRACKER2_ERROR_ALREADY_INITIALIZED; + InitializeSession session; + if (mode == TrackerMode) std::memset(parameter->workMemory, 0, WorkMemorySize(mode)); + if (mode != SystemClientMode) { + if (!OptionModeAllowed(parameter->optionMode)) return session.Abort(VR_TRACKER2_ERROR_INVALID_ARGUMENT); + if (const std::int32_t result = session.OpenTracking(); result != SCE_OK) return session.Abort(result); + if (const std::int32_t result = session.StartTracking(); result != SCE_OK) return session.Abort(result); + if (const std::int32_t result = session.OpenLedDetector(); result != SCE_OK) return session.Abort(result); + if (const std::int32_t result = session.StartLedDetector(); result != SCE_OK) return session.Abort(result); + } + session.Rollback(); NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/tests/GuestVrTracker2.cpp b/core/libs/tests/GuestVrTracker2.cpp index 0bbe3a869e..5649d09427 100644 --- a/core/libs/tests/GuestVrTracker2.cpp +++ b/core/libs/tests/GuestVrTracker2.cpp @@ -1,12 +1,17 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "ProcessParametersFixture.hpp" +#include #include #include #include #include #include +#include +#include #include +#include +#include extern "C" { std::int32_t APS5_VABI VrTracker2NotInitialized(); @@ -18,16 +23,34 @@ std::int32_t APS5_VABI VrTracker2Accept(); std::int32_t APS5_VABI VrTracker2FillDefaults(void* output); std::int32_t APS5_VABI VrTracker2ClearWord(void* output); std::int32_t APS5_VABI sceVrTracker2QueryMemory(const void* parameter, void* requirement); -struct VrTracker2InitializeParameter; +struct VrTracker2InitializeParameter { + std::uint32_t size; + std::uint32_t mode; + std::int32_t threadPriorities[2]; + std::uint64_t threadAffinityMasks[2]; + void* workMemory; + std::uint32_t workMemorySize; + std::uint32_t workMemoryAlignment; + std::uint32_t option; + std::uint32_t coordinateSystem; + std::uint8_t disableOption; + std::int8_t optionMode; + std::uint8_t reserved[14]; +}; std::int32_t APS5_VABI sceVrTracker2Initialize(const VrTracker2InitializeParameter* parameter); +int APS5_VABI getpid_nid_postfix(void); } +static_assert(sizeof(VrTracker2InitializeParameter) == 72); + namespace { alignas(16) std::array process{}; constexpr std::int32_t NotInitialized = static_cast(0x8A740001u); constexpr std::int32_t InvalidArgument = static_cast(0x8A740006u); +constexpr std::uint32_t TrackerMemory = 32u << 20; +constexpr std::size_t WorkMemoryTail = 64u << 10; void Require(bool condition, const char* message) { if (!condition) { @@ -58,6 +81,121 @@ bool Throws(TCall call) { return false; } +VrTracker2InitializeParameter TrackerParameter(std::vector& memory) { + VrTracker2InitializeParameter parameter{}; + parameter.size = sizeof(parameter); + parameter.threadPriorities[0] = 256; + parameter.threadPriorities[1] = 767; + parameter.threadAffinityMasks[0] = 0x1fff; + parameter.workMemory = memory.data(); + parameter.workMemorySize = static_cast(memory.size()); + parameter.workMemoryAlignment = 0x10000; + return parameter; +} + +bool WorkMemoryCleared(const std::vector& memory) { + const auto* middle = memory.data() + TrackerMemory / 2; + return AllEqual(middle, middle + 4096, 0) && AllEqual(memory.data() + TrackerMemory, memory.data() + memory.size(), 0xAA); +} + +bool WorkMemoryUntouched(const std::vector& memory) { + return AllEqual(memory.data(), memory.data() + memory.size(), 0xAA); +} + +std::int32_t InitializeFresh(std::vector& memory, const VrTracker2InitializeParameter& parameter) { + std::fill(memory.begin(), memory.end(), 0xAA); + return sceVrTracker2Initialize(¶meter); +} + +void CheckInitializeValidation(std::vector& memory) { + const auto valid = TrackerParameter(memory); + Require(sceVrTracker2Initialize(nullptr) == InvalidArgument, "initialize null"); + for (const std::uint32_t size : {0u, 71u, 73u}) { + auto parameter = valid; + parameter.size = size; + parameter.mode = 1000; + Require(InitializeFresh(memory, parameter) == InvalidArgument, "initialize size before mode"); + } + for (const std::uint32_t mode : {1u, 999u, 1002u, 0xffffffffu}) { + auto parameter = valid; + parameter.mode = mode; + Require(InitializeFresh(memory, parameter) == InvalidArgument, "initialize mode"); + } + for (const std::uint32_t mode : {1000u, 1001u}) { + auto parameter = valid; + parameter.mode = mode; + parameter.workMemory = nullptr; + Require(Throws([&] { sceVrTracker2Initialize(¶meter); }), "initialize system mode category"); + } + auto parameter = valid; + parameter.workMemory = nullptr; + Require(InitializeFresh(memory, parameter) == InvalidArgument, "initialize work memory null"); + parameter = valid; + parameter.workMemorySize = TrackerMemory - 1; + Require(InitializeFresh(memory, parameter) == InvalidArgument, "initialize work memory size"); + parameter = valid; + parameter.workMemoryAlignment = 0xffff; + Require(InitializeFresh(memory, parameter) == InvalidArgument, "initialize alignment"); + for (std::size_t index = 0; index < sizeof(parameter.reserved); ++index) { + parameter = valid; + parameter.reserved[index] = 1; + Require(InitializeFresh(memory, parameter) == InvalidArgument, "initialize reserved byte"); + } + for (std::size_t index = 0; index < 2; ++index) { + for (const std::int32_t priority : {255, 768, -1}) { + parameter = valid; + parameter.threadPriorities[index] = priority; + Require(InitializeFresh(memory, parameter) == InvalidArgument, "initialize thread priority"); + } + for (const std::uint64_t mask : {std::uint64_t{0x2000}, std::uint64_t{0x3fff}, std::uint64_t{1} << 63}) { + parameter = valid; + parameter.threadAffinityMasks[index] = mask; + Require(InitializeFresh(memory, parameter) == InvalidArgument, "initialize affinity mask"); + } + } + Require(WorkMemoryUntouched(memory), "initialize validation leaves work memory"); +} + +void CheckInitializeTracking(std::vector& memory, std::uint32_t sdkVersion) { + const auto valid = TrackerParameter(memory); + Require(InitializeFresh(memory, valid) == -1, "initialize tracking start result"); + Require(WorkMemoryCleared(memory), "initialize clears tracker memory"); + Require(sceVrTracker2Finalize() == NotInitialized, "initialize failure leaves module uninitialized"); + Require(VrTracker2NotInitialized() == NotInitialized, "initialize failure keeps uninitialized answers"); + Require(InitializeFresh(memory, valid) == -1, "initialize retry after failure"); + auto parameter = valid; + parameter.workMemoryAlignment = 0x10001; + parameter.option = 0xffffffffu; + parameter.coordinateSystem = 2; + parameter.disableOption = 1; + parameter.threadAffinityMasks[1] = 0x1fff; + Require(InitializeFresh(memory, parameter) == -1, "initialize late checks follow tracking"); + for (const std::int8_t optionMode : {std::int8_t{-1}, std::int8_t{0}}) { + parameter = valid; + parameter.optionMode = optionMode; + Require(InitializeFresh(memory, parameter) == -1, "initialize accepted option mode"); + } + for (const std::int8_t optionMode : {std::int8_t{2}, std::int8_t{-2}, std::int8_t{127}}) { + parameter = valid; + parameter.optionMode = optionMode; + Require(InitializeFresh(memory, parameter) == InvalidArgument, "initialize option mode"); + Require(WorkMemoryCleared(memory), "initialize option mode follows clearing"); + } + parameter = valid; + parameter.optionMode = 1; + Require(InitializeFresh(memory, parameter) == (sdkVersion >= 0x7000000 ? -1 : InvalidArgument), "initialize SDK option mode"); + Require(sceVrTracker2Finalize() == NotInitialized, "initialize leaves module uninitialized"); +} + +std::filesystem::path EnterApplicationRoot() { + const auto root = std::filesystem::temp_directory_path() / ("guest_vr_tracker2_" + std::to_string(getpid_nid_postfix())); + std::filesystem::remove_all(root); + std::filesystem::create_directories(root / "app0" / "sce_sys"); + std::ofstream(root / "app0" / "sce_sys" / "param.json") << "{\n \"contentId\": \"IV0000-ABCD00000_00-VRTRACKER2000000\",\n \"localizedParameters\": {\"defaultLanguage\": \"en-US\", \"en-US\": {\"titleName\": \"VrTracker2\"}},\n \"titleId\": \"ABCD00000\"\n}\n"; + std::filesystem::current_path(root); + return root; +} + } int main() { @@ -138,6 +276,15 @@ int main() { parameter[0] = 0x21; Require(sceVrTracker2QueryMemory(parameter.data(), requirement.data()) == InvalidArgument, "query size"); - Require(Throws([] { sceVrTracker2Initialize(nullptr); }), "unknown export"); + std::vector memory(TrackerMemory + WorkMemoryTail); + CheckInitializeValidation(memory); + const auto previous = std::filesystem::current_path(); + const auto root = EnterApplicationRoot(); + for (const std::uint32_t version : {0x6ffffffu, 0x7000000u}) { + std::memcpy(process.data() + 0x10, &version, sizeof(version)); + CheckInitializeTracking(memory, version); + } + std::filesystem::current_path(previous); + std::filesystem::remove_all(root); return 0; } diff --git a/docs/dev/debt/libSceVrTracker2.md b/docs/dev/debt/libSceVrTracker2.md index 29b19cc61c..c7c0424754 100644 --- a/docs/dev/debt/libSceVrTracker2.md +++ b/docs/dev/debt/libSceVrTracker2.md @@ -6,7 +6,7 @@ ### Unknown function info -- [sceVrTracker2Initialize](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - the 72-byte parameter layout is recovered, including thread priorities, CPU affinity masks, work memory, size/alignment and coordinate system. Meanings of the 32-bit option at 0x30 and byte options at 0x38/0x39 remain unresolved; initialization remains unimplemented. +- [sceVrTracker2Initialize](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - never initializes: after validation it returns the first non-zero libSceHmd2 tracking or LED-detector open/start result and resets to uninitialized, and throws if all four succeed or for modes 1000/1001, whose application category the host lacks. Unverified: the mode 0 category query is assumed to succeed, the `sceKernelGetAppInfo2` attribute checks are skipped (the host has no such record; a zeroed one passes), the daemon IPC client and work-memory heap are not created, rollback only undoes what the call opened, and the meanings of the options at 0x30, 0x38 and 0x39 remain unknown (0x39 is only range-checked). Firmware 13.60 libSceVrTracker2.sprx, read from disassembly, not measured on a console; the -1 a no-headset host gets relies on libSceHmd2's measured pre-initialization answers. PPSA05666 calls it with no headset and, with #7463, #7690 and #7658 applied, stopped on the NotImplemented. - [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - the seven anonymous throwing exports now have their consumed argument slots restored. Calibration output (`jRmuXjsknsI`) uses a 32-bit capacity with 608/1216-byte variants; its fields remain unresolved. Mesh requirements (`NBhWy+Gl0tY`) contain four 32-bit counts/sizes. The mode/buffer/capacity/size export (`9R6W33Au4B4`) uses 64-bit sizes. The 32/48-byte result parameters are recovered; result output and pose parameters still need full layouts; the telemetry input (`oZDm1bynCaA`) spans 7024 bytes. Their bodies remain unimplemented. - [Anonymous tracking queries](../../../core/libs/prx/libSceVrTracker2/Export.cpp) still lack initialized behavior. The paired option setter/getter consume 28/40-byte records with three integer options, a reset byte and one/two float vectors; option meanings remain unresolved. The scalar query uses an 8-byte size/float record. Diagnostic output spans 4096 bytes, extended output 0xD02E8 bytes, and tracking configuration input 1024 bytes; their fields remain unresolved. The relative-pose query uses two coordinate-system selectors and a 64-byte output containing a pose and 32 cleared bytes; the counter query uses a 64-bit output and a 32-bit selector whose meaning remains unresolved. Remaining aliases still need individual signatures. From a293f7901bee0c9ae1aa6b01062e67360633428d Mon Sep 17 00:00:00 2001 From: mrfelek Date: Fri, 23 Oct 2026 18:26:29 +0000 Subject: [PATCH 197/428] perf(agc): stage written-only swept elements on discrete GPUs On a discrete GPU a write-only V# element whose stride is wider than the widest buffer store (16 bytes) and that a direct dispatch sweeps is staged in a device-local shadow of up to 256 MiB, as read and written swept elements are. Its stores no longer cross the bus into the host import. AI-assisted. Tested by me on my own machine. --- .../Graphics/src/GuestBufferMemory.cpp | 3 ++- .../libSceAgcDriver/Graphics/src/ShaderResources.cpp | 3 ++- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 12 ++++++++---- docs/dev/debt/libSceAgcDriver.md | 1 + 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index c0d73d331b..1990121742 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -4385,6 +4385,7 @@ VkBufferUsageFlags gpuCopyUsage(bool addressable) { // window takes those elements there. APS5_ATOMIC_STAGE_MAX_KIB (1024) bounds the atomic one. constexpr std::uint64_t SweptShadowMax = std::uint64_t{16} << 20u; constexpr std::uint64_t DiscreteWrittenShadowMax = std::uint64_t{8} << 20u; +constexpr std::uint64_t DiscreteSweptShadowMax = std::uint64_t{256} << 20u; bool writtenShadowEnabled() { static const bool disabled = std::getenv("APS5_NO_WRITTEN_SHADOW") != nullptr; @@ -4527,7 +4528,7 @@ bool GuestBufferMemory::stagingEligible(const Region& region, bool addressable) const auto bytes = region.end - region.begin; if (!WritesOverlap(region.begin, static_cast(bytes))) return false; if (region.atomic && atomicStagingEnabled() && bytes <= atomicStageMax()) return true; - return writtenShadowEnabled() && bytes >= writtenShadowMin() && bytes <= (region.swept ? std::max(writtenShadowMax(context.discreteGpu), SweptShadowMax) : writtenShadowMax(context.discreteGpu)); + return writtenShadowEnabled() && bytes >= writtenShadowMin() && bytes <= (region.swept ? std::max(writtenShadowMax(context.discreteGpu), context.discreteGpu ? DiscreteSweptShadowMax : SweptShadowMax) : writtenShadowMax(context.discreteGpu)); } bool GuestBufferMemory::overlayCandidate(const Region& inside, bool readOnly) const { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 9833032de0..928df3522b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -4384,7 +4384,8 @@ std::size_t ShaderResources::addGuestBuffer(std::span words Require(!written || !overlap(address, size, indexAddress, indexBytes), "writable shader buffer aliases the index buffer"); written = written || allWritten; const std::uint64_t recordBytes = descriptor.Stride() != 0 ? descriptor.Stride() : extent; - const bool swept = read && dispatchThreads != 0 && recordBytes != 0 && dispatchThreads * recordBytes * 2u >= byteSize; + constexpr std::uint64_t WidestBufferStoreBytes = 16; + const bool swept = (read || (context.discreteGpu && descriptor.Stride() > WidestBufferStoreBytes)) && dispatchThreads != 0 && recordBytes != 0 && dispatchThreads * recordBytes * 2u >= byteSize; if (written) guestMemory.AddWritable(address, size, atomic, swept); else { guestMemory.AddReadable(address, size); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 0c14df96b6..1a4e1bfa70 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -3430,7 +3430,7 @@ bool readWrittenStagingTests(const Device& device, Recorder& recorder) { std::cout << "host imports unavailable: read and written staging not tested\n"; return false; } - constexpr std::size_t bytes = 16u << 20u; + constexpr std::size_t bytes = 40u << 20u; #ifdef _WIN32 void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); #else @@ -3463,9 +3463,8 @@ bool readWrittenStagingTests(const Device& device, Recorder& recorder) { Require(boundInPlace(address + 4096, 4u << 20u, false) != context.discreteGpu, context.discreteGpu ? "a written-only 4 MiB element was bound in place on a discrete GPU" : "a written-only 4 MiB element was staged past the written window"); Require(boundInPlace(address + 4096, 12u << 20u, false), "a written-only 12 MiB element was staged past the discrete written window"); Require(!boundInPlace(address + 4096, 4u << 20u, true), "a read and written 4 MiB element was bound in place"); - const auto buildInPlace = [&](std::uint64_t dispatchThreads, bool read, std::uint32_t stride, std::uint32_t extent) { + const auto buildInPlace = [&](std::uint64_t dispatchThreads, bool read, std::uint32_t stride, std::uint32_t extent, std::uint32_t elementBytes = 12u << 20u) { const auto element = address + 4096; - constexpr std::uint32_t elementBytes = 12u << 20u; const std::uint32_t records = stride != 0 ? elementBytes / stride : elementBytes; ShaderRecompiler::RecompileResult program; ShaderRecompiler::DescriptorBinding binding; @@ -3490,7 +3489,12 @@ bool readWrittenStagingTests(const Device& device, Recorder& recorder) { Require(!buildInPlace((12u << 20u) / 16u, true, 16, 0), "a dispatch sweeping a read and written 12 MiB element bound it in place"); Require(buildInPlace(1024, true, 16, 0), "a dispatch touching 16 KiB of a read and written 12 MiB element staged all of it"); Require(buildInPlace(0, true, 16, 0), "a dispatch of unknown size staged a read and written 12 MiB element"); - Require(buildInPlace((12u << 20u) / 16u, false, 16, 0), "a dispatch sweeping a written-only 12 MiB element staged it"); + Require(buildInPlace((12u << 20u) / 32u, false, 32, 0) != context.discreteGpu, context.discreteGpu ? "a dispatch sweeping a written-only 12 MiB element of 32-byte records bound it in place on a discrete GPU" : "a dispatch sweeping a written-only 12 MiB element of 32-byte records staged it on a GPU without separate VRAM"); + Require(buildInPlace((12u << 20u) / 16u, false, 16, 0), "a dispatch sweeping a written-only 12 MiB element of 16-byte records, which its stores write contiguously, staged it"); + Require(buildInPlace(1024, false, 32, 0), "a dispatch touching 32 KiB of a written-only 12 MiB element staged all of it"); + Require(buildInPlace((12u << 20u) / 32u, false, 0, 32), "a dispatch writing a written-only 12 MiB raw element staged it"); + Require(buildInPlace(204800, false, 160, 0, 204800u * 160u) != context.discreteGpu, context.discreteGpu ? "a dispatch sweeping a written-only 31.25 MiB element of 160-byte records bound it in place on a discrete GPU" : "a dispatch sweeping a written-only 31.25 MiB element of 160-byte records staged it on a GPU without separate VRAM"); + Require(buildInPlace(204800, true, 160, 0, 204800u * 160u) != context.discreteGpu, context.discreteGpu ? "a dispatch sweeping a read and written 31.25 MiB element bound it in place on a discrete GPU" : "a dispatch sweeping a read and written 31.25 MiB element staged it past the 16 MiB cap on a GPU without separate VRAM"); Require(!buildInPlace((12u << 20u) / 32u, true, 0, 32), "a dispatch sweeping a read and written 12 MiB raw element bound it in place"); Require(buildInPlace(1024, true, 0, 32), "a dispatch touching 32 KiB of a read and written 12 MiB raw element staged all of it"); Require(buildInPlace((12u << 20u) / 32u, true, 0, 0), "a raw element without an access extent was staged"); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 63766b2d7e..bb50d86489 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -253,6 +253,7 @@ - [Long Windows pipeline-cache paths](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp): `readHeader` and the failed-read existence check still use ordinary paths. A filename beyond MAX_PATH can prevent header reuse or be reported missing; the shared content reader and journal normalization do not cover these calls. - [DB_RENDER_CONTROL.STENCIL_CLEAR_ENABLE without a stencil plane](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): a stencil clear on a depth target whose `DB_STENCIL_INFO` format is `STENCIL_INVALID` is dropped; the depth clear and color writes proceed. AMD PAL ([`gfx9RsrcProcMgr.cpp`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32/src/core/hw/gfxip/rpm/gfx9/gfx9RsrcProcMgr.cpp)) and radeonsi (`si_clear.c`) never set the bit without a stencil plane, and shadPS4 drops it the same way (`vk_rasterizer.cpp`, `StencilValid`). No source states what the hardware does with the bit then; the console was not measured. A read-only stencil plane still throws +- [Swept write-only staging](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): on a discrete GPU a write-only element whose V# stride is wider than the widest buffer store (16 bytes) and that a direct dispatch sweeps is staged in a device-local shadow. The shadow still takes a copy-in, because the copy-back covers the whole element and the stores may leave part of each record unwritten. A sweep proven to write every byte of the element (from the offsets and widths of its stores within the record) could skip the copy-in, about 10 ms per frame in Diablo II: Resurrected; that proof is not made. Store widths are not recorded per instruction either, so a record of 16 bytes or less stays in place even when its stores are narrower. ### Opt-in draw paths From 16c9c3174edd6c172181f1f67d3234c43ede7a4a Mon Sep 17 00:00:00 2001 From: hypermist Date: Sat, 24 Oct 2026 23:30:29 +1300 Subject: [PATCH 198/428] test(libSceVrTracker2): check the whole cleared work memory range Scan every byte of the 32 MiB tracker range and the tail after it, so a shorter clear or one that stops before the boundary fails. Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestVrTracker2.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/libs/tests/GuestVrTracker2.cpp b/core/libs/tests/GuestVrTracker2.cpp index 5649d09427..1c01bf1332 100644 --- a/core/libs/tests/GuestVrTracker2.cpp +++ b/core/libs/tests/GuestVrTracker2.cpp @@ -94,8 +94,7 @@ VrTracker2InitializeParameter TrackerParameter(std::vector& memory } bool WorkMemoryCleared(const std::vector& memory) { - const auto* middle = memory.data() + TrackerMemory / 2; - return AllEqual(middle, middle + 4096, 0) && AllEqual(memory.data() + TrackerMemory, memory.data() + memory.size(), 0xAA); + return AllEqual(memory.data(), memory.data() + TrackerMemory, 0) && AllEqual(memory.data() + TrackerMemory, memory.data() + memory.size(), 0xAA); } bool WorkMemoryUntouched(const std::vector& memory) { From 412c3cd91f3aa7563417fc512813527cb3535646 Mon Sep 17 00:00:00 2001 From: ZSWolt <87244144+ZSWolt@users.noreply.github.com> Date: Sat, 24 Oct 2026 13:27:49 +0300 Subject: [PATCH 199/428] fix(libSceNet): print the console's line for a negative sceNetUsleep After sceNetInit, the console's sceNetUsleep prints "[LIBNET] usleep " to standard output for a negative count before failing with EINVAL, as a payload measured on firmware 13.60 (#9076). AnyPS5 failed without printing it. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNet/Export.cpp | 6 ++++- core/libs/tests/GuestSceNet.cpp | 41 ++++++++++++++++++++++++++---- docs/dev/debt/libSceNet.md | 2 +- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index de6902d802..e30f0caa81 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -2615,7 +2615,11 @@ int APS5_VABI sceNetThreadJoin(void* thread) { int APS5_VABI sceNetUsleep(int microseconds) { if (!g_net_inited) return fail(NET_ENOTINIT); - if (microseconds < 0) return fail(NET_EINVAL); + if (microseconds < 0) { + std::fprintf(stdout, "[LIBNET] usleep %d\n", microseconds); + std::fflush(stdout); + return fail(NET_EINVAL); + } const int result = sceKernelUsleep_nid_postfix(static_cast(microseconds)); if (result < 0) set_errno((result & 0xFF00) == 0x100 ? result & 0xFF : 205); return result; diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 9a05372a89..d20974f2d7 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -172,6 +172,34 @@ static bool Failed(std::int64_t result, int error) { return result == static_cast(0x80410100u | static_cast(error)) && *sceNetErrnoLoc() == error; } +template +static std::string CaptureStdout(TBody body) { + std::fflush(stdout); + std::FILE* capture = std::tmpfile(); + Require(capture != nullptr); +#ifdef _WIN32 + const int saved = _dup(_fileno(stdout)); + Require(saved >= 0 && _dup2(_fileno(capture), _fileno(stdout)) == 0); +#else + const int saved = dup(fileno(stdout)); + Require(saved >= 0 && dup2(fileno(capture), fileno(stdout)) >= 0); +#endif + body(); + std::fflush(stdout); +#ifdef _WIN32 + Require(_dup2(saved, _fileno(stdout)) == 0); + _close(saved); +#else + Require(dup2(saved, fileno(stdout)) >= 0); + close(saved); +#endif + std::string text; + std::rewind(capture); + for (int character = std::fgetc(capture); character != EOF; character = std::fgetc(capture)) text.push_back(static_cast(character)); + std::fclose(capture); + return text; +} + static void CheckSocketHostErrors() { Require(Failed(sceNetSocket(nullptr, 2, 1, 17), 43)); } @@ -1123,14 +1151,17 @@ static void CheckSystemTime() { } static void CheckUsleep() { - Require(sceNetUsleep(0) == 0); + Require(CaptureStdout([] { Require(sceNetUsleep(0) == 0); }).empty()); const auto start = std::chrono::steady_clock::now(); Require(sceNetUsleep(2000) == 0); Require(std::chrono::steady_clock::now() - start >= std::chrono::microseconds(2000)); - *sceNetErrnoLoc() = 0; - Require(Failed(sceNetUsleep(-1), 22)); - *sceNetErrnoLoc() = 0; - Require(Failed(sceNetUsleep(INT32_MIN), 22)); + const std::string printed = CaptureStdout([] { + *sceNetErrnoLoc() = 0; + Require(Failed(sceNetUsleep(-1), 22)); + *sceNetErrnoLoc() = 0; + Require(Failed(sceNetUsleep(INT32_MIN), 22)); + }); + Require(printed == "[LIBNET] usleep -1\n[LIBNET] usleep -2147483648\n"); } static void CheckEpollIdent() { diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 847cc58ce0..cf493287ba 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -27,7 +27,7 @@ ### Functional - [sceNetRecv/sceNetRecvfrom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) translate Windows WSAEMSGSIZE into the copied byte count for datagram sockets, including MSG_PEEK, and retain the sender address. This follows FreeBSD 11.4 recv(2) and Winsock's documented truncated-datagram behavior; the console measurement covered only libkernel_sys `recv` and `recvfrom`, not libSceNet. -- [sceNetUsleep](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) prints nothing for a negative count, where the console prints `[LIBNET] usleep ` to standard output before failing with `EINVAL`. What an interrupted sleep returns is unmeasured: AnyPS5 returns the result of libkernel's `sceKernelUsleep`, a cancellation point. The signed 32-bit count (the upper half of the register is ignored) and the 0 result were measured on a PS5 (firmware 13.60) +- [sceNetUsleep](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - whether the console prints its `[LIBNET] usleep ` line for a negative count before `sceNetInit` is unmeasured; nothing is printed here. What an interrupted sleep returns is unmeasured: AnyPS5 returns the result of libkernel's `sceKernelUsleep`, a cancellation point. The signed 32-bit count (the upper half of the register is ignored) and the 0 result were measured on a PS5 (firmware 13.60) - [sceNetIoctl](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - on a `sceNetSocket` socket only `FIONBIO` and `FIONREAD` are handled and other requests fail with `ENOTTY`, as in libkernel's `ioctl`, though FreeBSD 11.4's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L156-L262) handles more; not measured on a console. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/60c42b925c3d1e00008621b8587819999260ec31/src/core/libraries/net/net.cpp#L1839) (PS4) returns `EINVAL` for other requests. `FIONBIO` sets `SO_NBIO`, assumed to be `soo_ioctl`'s `SS_NBIO`. `FIONREAD` asks the host, which on Linux counts only the next datagram where `soo_ioctl` counts all. Libkernel's own sockets go to its `ioctl`; other descriptors get `EBADF`. - [sceNetResolverStartAton6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reverse-resolves a 16-byte IPv6 address through the host resolver (`getnameinfo` with `NI_NAMEREQD`) into the caller's buffer of `len` bytes, as `sceNetResolverStartAton` does for IPv4. The `(rid, addr, hostname, len, timeout, retry, flags)` signature comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L992-L997). An address with no name, or a name longer than `len - 1` bytes, fails with `RESOLVER_ENODNS` (`0x804101E1`) and leaves the buffer untouched; whether the console truncates instead is not verified. Abort flag 2 (ATON preservation) fails the next call with `EINTR` as it does `sceNetResolverStartAton`. Timeout and retry are ignored, and any nonzero flags throw because their meaning is unknown - [sceNetAccept](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) inherits the listener's non-blocking mode through the existing `SO_NBIO` setter, as the libkernel socket path does and FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L571) copies the socket state. The mode is sampled when accept starts; changes between connection creation and accept are not modelled. Not measured on a console. From 546bad013f7a145261dbedc49a6169c49002b404 Mon Sep 17 00:00:00 2001 From: hakarrr Date: Sat, 24 Oct 2026 16:47:37 +0530 Subject: [PATCH 200/428] fix(tools): open the unwrap_self temporary file through io.open --- tools/unwrap_self.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/unwrap_self.py b/tools/unwrap_self.py index 8181e49ddc..484334e200 100644 --- a/tools/unwrap_self.py +++ b/tools/unwrap_self.py @@ -30,6 +30,7 @@ usage: unwrap_self.py [--verbose] """ import argparse +import io import math import secrets import struct @@ -329,7 +330,7 @@ def unwrap(path, output, verbose=False): destination.parent.mkdir(parents=True, exist_ok=True) temporary_path = destination.parent / (".unwrap-self-" + secrets.token_hex(16)) - temporary = temporary_path.open("xb") + temporary = io.open(temporary_path, "xb") try: with temporary: temporary.write(result) From dad627e00c6515f82a33971fe1dc59b21996ad09 Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Sat, 24 Oct 2026 10:23:57 +0000 Subject: [PATCH 201/428] test(tools): run debt_split in process and commit only where tests need it test_debt_split.py started debt_split.py as a subprocess for every split and committed the index in setUp and after each split, although only the rebase tests and the resolve test outside a rebase depend on a commit. The split helper now calls main() in process from the fixture's directory with its output sent to a sink, the commits move into those tests, and the test's git commands skip the automatic maintenance subprocess. One test keeps a split through the command line to cover its output, and resolve stays a subprocess. The test starts about a third fewer processes and takes half the time on Linux; Windows CI reported 34 s for it. --- tools/tests/test_debt_split.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/tools/tests/test_debt_split.py b/tools/tests/test_debt_split.py index 1f2cf31775..ae68754774 100644 --- a/tools/tests/test_debt_split.py +++ b/tools/tests/test_debt_split.py @@ -44,15 +44,14 @@ class DebtSplitTests(unittest.TestCase): self.root = Path(self.directory.name) self.git("init", "-q", "-b", "main") self.write(INDEX, ORIGINAL) - self.git("add", ".") - self.git("commit", "-qm", "docs: record technical debt") def tearDown(self): self.directory.cleanup() def git(self, *args, check=True): return subprocess.run( - ["git", "-c", "user.name=Debt Test", "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], + ["git", "-c", "user.name=Debt Test", "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", + "-c", "maintenance.auto=false", *args], cwd=self.root, capture_output=True, text=True, check=check, env={**os.environ, "GIT_EDITOR": "true"}, ) @@ -68,10 +67,19 @@ class DebtSplitTests(unittest.TestCase): return subprocess.run([sys.executable, str(TOOL), command], cwd=self.root, capture_output=True, text=True, timeout=20) def split(self): - result = self.tool("split") - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + directory, streams = os.getcwd(), (sys.stdout, sys.stderr) + os.chdir(self.root) + try: + with open(os.devnull, "w", encoding="utf-8") as sink: + sys.stdout = sys.stderr = sink + debt_split.main(["split"]) + finally: + os.chdir(directory) + sys.stdout, sys.stderr = streams + + def commit(self, message): self.git("add", "-A") - self.git("commit", "-qm", "docs: split technical debt") + self.git("commit", "-qm", message) def test_split_preserves_external_urls_and_rewrites_local_links(self): links = ["HTTPS://example.test/guide#intro", "//example.test/guide#intro", @@ -114,11 +122,13 @@ class DebtSplitTests(unittest.TestCase): self.assertEqual(self.read(INDEX).count("[debt](debt/)"), 1) def rebase(self, stubs, functional): + self.commit("docs: record technical debt") self.git("checkout", "-qb", "change") self.write(INDEX, index(stubs, functional)) self.git("commit", "-qam", "feat: change debt") self.git("checkout", "-q", "main") self.split() + self.commit("docs: split technical debt") self.git("checkout", "-q", "change") rebase = self.git("rebase", "main", check=False) self.assertNotEqual(rebase.returncode, 0, rebase.stdout + rebase.stderr) @@ -170,11 +180,13 @@ class DebtSplitTests(unittest.TestCase): self.assertIn("<<<<<<<", self.read(INDEX)) def test_resolve_refuses_an_entry_main_changed_too(self): + self.commit("docs: record technical debt") self.git("checkout", "-qb", "change") self.write(INDEX, index([A_STUB, B_STUB + " here", C_STUB], [A_FUNCTIONAL, SHADER, SHARED])) self.git("commit", "-qam", "feat: change b") self.git("checkout", "-q", "main") self.split() + self.commit("docs: split technical debt") path = "docs/dev/debt/libSceB.md" self.write(path, self.read(path).replace("returns 0", "returns 1")) self.git("commit", "-qam", "docs: change b") @@ -185,6 +197,7 @@ class DebtSplitTests(unittest.TestCase): self.assertIn("entry not found", result.stderr) def test_resolve_needs_a_conflict(self): + self.commit("docs: record technical debt") result = self.tool("resolve") self.assertNotEqual(result.returncode, 0) self.assertIn("not in a three-way conflict", result.stderr) From 1613faaaf2142b3a6bc4608aa2fc0f583fb4f2a0 Mon Sep 17 00:00:00 2001 From: al3ss4ndrolb <173555546+al3ss4ndrolb@users.noreply.github.com> Date: Sat, 24 Oct 2026 13:27:34 +0200 Subject: [PATCH 202/428] test(tools): look up the environment-selected NID database in an empty source tree Co-Authored-By: Claude Opus 5.5 --- tools/tests/test_nid_names.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/tests/test_nid_names.py b/tools/tests/test_nid_names.py index f5ccf2945d..f14b4be874 100644 --- a/tools/tests/test_nid_names.py +++ b/tools/tests/test_nid_names.py @@ -41,8 +41,13 @@ class DatabaseCacheTests(unittest.TestCase): database.write_text(f"{nid} sceExample\n", encoding="utf-8") scratch = str(Path(directory) / "scratch") Path(scratch).mkdir() + tree = Path(directory) / "tree" + (tree / "core" / "libs" / "prx").mkdir(parents=True) + (tree / "tools").mkdir() + tool = tree / "tools" / "nid_names.py" + tool.write_bytes(Path(script).read_bytes()) environment = {**os.environ, "ANYPS5_NID_DB": str(database), "TEMP": scratch, "TMP": scratch, "TMPDIR": scratch} - result = subprocess.run([sys.executable, script, "--nid", nid, "--json"], capture_output=True, text=True, + result = subprocess.run([sys.executable, str(tool), "--nid", nid, "--json"], capture_output=True, text=True, env=environment, timeout=20) self.assertEqual(list(Path(scratch).iterdir()), []) self.assertEqual(result.returncode, 0, result.stderr) From 028136ed0f0cc85eec6edea422df2a79e5c16ffb Mon Sep 17 00:00:00 2001 From: JaceTheBlu Date: Sat, 24 Oct 2026 13:57:19 +0200 Subject: [PATCH 203/428] fix(web): show batch file and module counts as numbers The batch summary passed the total file and module counts through the byte formatter, so two apps with 1519 files and 5 modules showed "1.5 KiB files" and "5 B modules". Counts are now printed as numbers, as the per-app list and the single-app summary already do. Co-Authored-By: Claude Opus 5.5 --- demo/web/app.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/demo/web/app.mjs b/demo/web/app.mjs index 4e0b534145..cfe101fc32 100644 --- a/demo/web/app.mjs +++ b/demo/web/app.mjs @@ -67,8 +67,9 @@ function update() { const totalBytes = sum(summaries.map(summary => summary.totalBytes)); const modules = sum(summaries.map(summary => summary.modules)); const sizeText = value => value === null ? "beyond the supported size range" : bytes(value); + const countText = value => value === null ? "beyond the supported range" : value; $("archive-info").textContent = multi - ? `${selectedInputs.length} apps · ${sizeText(totalFiles)} files · ${sizeText(totalBytes)} expanded · ${sizeText(modules)} modules · ${sizeText(selectedBytes)} ${target} conversion input` + ? `${selectedInputs.length} apps · ${countText(totalFiles)} files · ${sizeText(totalBytes)} expanded · ${countText(modules)} modules · ${sizeText(selectedBytes)} ${target} conversion input` : `${summaries[0].files} files · ${bytes(summaries[0].totalBytes)} expanded · ${summaries[0].modules} modules · ${bytes(target === "windows" ? summaries[0].windowsSelectedBytes : summaries[0].selectedBytes)} ${target} conversion input` + (summaries[0].root ? ` · Root: ${summaries[0].root}` : ""); } $("save-note").textContent = multi ? $("save").value === "memory" ? "Creates one ZIP with each app in its own folder. The complete bundle must fit in the 256 MiB memory-download budget." : "Writes one compressed ZIP containing each app in its own folder, without holding the whole bundle in memory." : $("save").value === "disk" ? "Creates a subfolder with the executable, libs and app0. No ZIP compression. Incomplete output folders are removed after cancellation or failure. Browsers may block DLL and other restricted files; use ZIP for those apps." : $("save").value === "zip" ? "Writes a compressed ZIP directly to disk without holding the whole archive in memory." : "Up to 256 MiB including expanded files and output overhead."; From 2fce9e473b0c841c911c261f35de58d45cadf3a2 Mon Sep 17 00:00:00 2001 From: hypermist Date: Sun, 25 Oct 2026 00:58:25 +1300 Subject: [PATCH 204/428] fix(libSceFont): check the font handle before reading it in the outline profile ComputeFontGlyphsOutlineProfile read font->library before checking the handle and cleared the output only after the context lookup, relying on sceFontGetFontGlyphsOutlineProfile to have validated the handle. It now rejects a null or non-font handle with SCE_FONT_ERROR_INVALID_FONT_HANDLE first, clearing a non-null output, then checks the output, then clears it before the library and context lookups, matching the firmware 13.60 order. The test also covers a null output with a bad handle and a handle with no library. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceFont/src/Layout.cpp | 6 +++++- core/libs/tests/GuestFontOutlineProfile.cpp | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceFont/src/Layout.cpp b/core/libs/prx/libSceFont/src/Layout.cpp index b013c7c79f..d297840675 100644 --- a/core/libs/prx/libSceFont/src/Layout.cpp +++ b/core/libs/prx/libSceFont/src/Layout.cpp @@ -603,11 +603,15 @@ int Font::ComputeFontStyleInformation(FontHandle handle, FontStyleInformation* i int Font::ComputeFontGlyphsOutlineProfile(FontHandle handle, FontOutlineProfile* profile) { auto* font = GetNativeFont(handle); + if (!font || font->magic != HANDLE_MAGIC) { + if (profile) *profile = {}; + return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + } if (!profile) return SCE_FONT_ERROR_INVALID_PARAMETER; + *profile = {}; auto* library = static_cast(font->library); auto* context = library ? FontContext(library, font) : nullptr; if (!context) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; - *profile = {}; const std::uint32_t fontId = font->open_info.ctx_entry_index; if (static_cast(fontId) < 0) return SCE_FONT_OK; const SysDriver* driver = library->sys_driver; diff --git a/core/libs/tests/GuestFontOutlineProfile.cpp b/core/libs/tests/GuestFontOutlineProfile.cpp index 3d9cca0efb..ba82b0d6fb 100644 --- a/core/libs/tests/GuestFontOutlineProfile.cpp +++ b/core/libs/tests/GuestFontOutlineProfile.cpp @@ -119,6 +119,16 @@ int main() { Require(sceFontGetFontGlyphsOutlineProfile(reinterpret_cast(notAFont), &profile) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && Zero(profile)); Require(sceFontGetFontGlyphsOutlineProfile(plain, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); Require(sceFontGetFontGlyphsOutlineProfile(nullptr, nullptr) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(sceFontGetFontGlyphsOutlineProfile(reinterpret_cast(notAFont), nullptr) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + auto* plainFont = reinterpret_cast(plain); + const FontLibrary owner = plainFont->library; + plainFont->library = nullptr; + Fill(profile); + Require(sceFontGetFontGlyphsOutlineProfile(plain, &profile) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && Zero(profile)); + Require(sceFontGetFontGlyphsOutlineProfile(plain, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + plainFont->library = owner; + Fill(profile); + Require(sceFontGetFontGlyphsOutlineProfile(plain, &profile) == SCE_FONT_OK && Zero(profile)); for (FontHandle font : {profiled, plain, shortMaxp}) Require(sceFontCloseFont(font) == SCE_FONT_OK); Require(sceFontDestroyLibrary(&library) == SCE_FONT_OK); From 0b701f2f2c0ec137f91580f3851d5317f896ebb7 Mon Sep 17 00:00:00 2001 From: harisibrar1 <207127467+harisibrar1@users.noreply.github.com> Date: Sat, 24 Oct 2026 01:44:08 -0400 Subject: [PATCH 205/428] fix(libc): reject a catch or upcast to a base that a private path repeats Bases() counted only base subobjects reached through public paths, so a class with the same base once publicly and once privately looked like it had a unique base. Catch matching, the RTTI upcast callbacks and __dynamic_cast's cross-cast then accepted it, where C++ requires an unambiguous public base and libsupc++'s __do_upcast/__do_dyncast reject it as ambiguous. Count every path to the base for ambiguity, and accept the single subobject only when at least one path to it is public. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/exception/Runtime.cpp | 7 +++++-- core/libs/tests/ExceptionRuntime.cpp | 4 ++++ core/libs/tests/WindowsExceptions.cpp | 11 +++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/exception/Runtime.cpp b/core/libs/prx/libc/src/exception/Runtime.cpp index e790d1a2db..6d1cd738bc 100644 --- a/core/libs/prx/libc/src/exception/Runtime.cpp +++ b/core/libs/prx/libc/src/exception/Runtime.cpp @@ -200,12 +200,15 @@ void VisitBases(const std::type_info* type, BaseLocation location, bool publicPa void Bases(const std::type_info* type, void* object, Search& search) { std::uintptr_t identity = 0; + bool reachedPublicly = false; auto visitor = [&](const std::type_info* candidate, BaseLocation location, bool publicPath) { - if (!publicPath || !Equal(candidate, search.wanted)) return; - if (!search.count) { search.found = location.object; identity = location.identity; search.count = 1; } + if (!Equal(candidate, search.wanted)) return; + if (!search.count) { search.found = location.object; identity = location.identity; search.count = 1; reachedPublicly = publicPath; } else if (search.found != location.object || identity != location.identity) search.count = 2; + else if (publicPath) reachedPublicly = true; }; VisitBases(type, {object, 0}, true, visitor); + if (search.count == 1 && !reachedPublicly) search.count = 0; } bool PointerMatch(const __cxxabiv1::__pbase_type_info* caught, const __cxxabiv1::__pbase_type_info* thrown, diff --git a/core/libs/tests/ExceptionRuntime.cpp b/core/libs/tests/ExceptionRuntime.cpp index 8f8b1c41fe..900037f876 100644 --- a/core/libs/tests/ExceptionRuntime.cpp +++ b/core/libs/tests/ExceptionRuntime.cpp @@ -63,6 +63,7 @@ struct Virtual : virtual Base {}; struct Left : Base {}; struct Right : Base {}; struct Repeated : Left, Right {}; +struct MixedRepeated : Left, private Right {}; struct LeftVirtual : virtual Base {}; struct RightVirtual : virtual Base {}; struct Diamond : LeftVirtual, RightVirtual {}; @@ -82,6 +83,9 @@ void CheckRtti() { try { throw static_cast(nullptr); } catch (Base*) { assert(false); } catch (Repeated* p) { assert(p == nullptr); } + try { throw MixedRepeated(); } + catch (Base&) { assert(false); } + catch (MixedRepeated&) {} try { throw static_cast(nullptr); } catch (Base* p) { assert(p == nullptr); } try { throw nullptr; } diff --git a/core/libs/tests/WindowsExceptions.cpp b/core/libs/tests/WindowsExceptions.cpp index c83acd49c2..5adb55cac4 100644 --- a/core/libs/tests/WindowsExceptions.cpp +++ b/core/libs/tests/WindowsExceptions.cpp @@ -56,6 +56,17 @@ static void TestTypeInfoVtables() { if (!upcastType(&multiple, &base, &object) || object != storage.data() + 16) throw std::runtime_error("multiple RTTI upcast failed"); object = storage.data(); if (catchType(&other, &single, &object, 0)) throw std::runtime_error("unrelated RTTI catch succeeded"); + TypeRecord shared {classTable + 2 * sizeof(void*), "6Shared"}; + SingleRecord publicPath {{singleTable + 2 * sizeof(void*), "10PublicPath"}, &shared}; + SingleRecord privatePath {{singleTable + 2 * sizeof(void*), "11PrivatePath"}, &shared}; + MultipleRecord mixed {{multipleTable + 2 * sizeof(void*), "5Mixed"}, 1, 2, {{&publicPath.type, 2}, {&privatePath.type, 16 << 8}}}; + MultipleRecord hidden {{multipleTable + 2 * sizeof(void*), "6Hidden"}, 0, 1, {{&privatePath.type, 0}, {}}}; + object = storage.data(); + if (catchType(&shared, &mixed, &object, 0) || object != storage.data()) throw std::runtime_error("RTTI catch of a base repeated through a private path succeeded"); + if (upcastType(&mixed, &shared, &object) || object != storage.data()) throw std::runtime_error("RTTI upcast to a base repeated through a private path succeeded"); + if (!catchType(&publicPath, &mixed, &object, 0) || object != storage.data()) throw std::runtime_error("RTTI catch of the public path failed"); + object = storage.data(); + if (catchType(&shared, &hidden, &object, 0)) throw std::runtime_error("RTTI catch of a privately inherited base succeeded"); } extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char*); From 5489f07047b787befdebcd3e5f77aa4c60caee92 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Thu, 22 Oct 2026 02:20:12 +0700 Subject: [PATCH 206/428] docs(libc): correct the RSIZE_MAX that swprintf_s and vswprintf_s assume Co-Authored-By: Claude --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index bb3d4b6fa8..f827eccd6d 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -42,7 +42,7 @@ - [memcpy_s, memmove_s, strcpy_s, strncpy_s, strcat_s, strncat_s, wmemcpy_s, wmemmove_s, wcsncpy_s, wcscat_s, wcsncat_s](../../../core/libs/prx/libc/src/Strings.cpp) (libc) use the C11 Annex K destination size constraints, with RSIZE_MAX taken as (SIZE_MAX >> 1) / sizeof(char16_t) for wide operations and SIZE_MAX >> 1 for byte operations. An oversized destination size returns ERANGE (34) without touching the destination; the console's RSIZE_MAX and exact error value are unverified. Constraint violations return an error without invoking the registered constraint handler, as before. - [wcsncat_s and wcscat_s](../../../core/libs/prx/libc/src/Strings.cpp) (libc) refuse a copy as overlapping only when the written range `[dest + len1, dest + len1 + len2)` meets the read range `[src, src + len2)`. The rule comes from a static reading of the console's `wcsncat_s` in `libc.prx` (not run on a console); `wcscat_s` gets it through `wcsncat_s`, and its own console code was not read. The narrow `strncat_s` keeps a wider rule, `[dest, dest + used + length + 1)` against `[src, src + length + 1)`, whose console code was not read either, so it refuses some copies whose wide equivalent proceeds. - [snwprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follows C11 K.3.9.1.3 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`. A runtime-constraint violation only returns a negative value: the handler installed with `set_constraint_handler_s` is not invoked, and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` -- [swprintf_s and vswprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follow C11 K.3.9.1.4 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`: the complete output, including the terminating null, must fit in the buffer, otherwise it is a runtime-constraint violation that sets `buffer[0]` to zero and returns a negative value, and the buffer is not partially written. Argument and encoding checks match `snwprintf_s` +- [swprintf_s and vswprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follow C11 K.3.9.1.4 with `RSIZE_MAX` assumed to be `(SIZE_MAX >> 1) / sizeof(char16_t)`: the complete output, including the terminating null, must fit in the buffer, otherwise it is a runtime-constraint violation that sets `buffer[0]` to zero and returns a negative value, and the buffer is not partially written. Argument and encoding checks match `snwprintf_s` - [strerror](../../../core/libs/prx/libc/src/Errors.cpp) (libc) - follows the `libc.prx` that CUSA04865 and CUSA12435 ship, loaded on a PS4 Pro (firmware 10.71): 0 gives "No error", 1 to 94 the FreeBSD texts, any other number "error_code=0x%08x", and errno is left alone. The console's system libc, which homebrew without a bundled `libc.prx` uses, answers differently ("No error information" for 0 and unknown numbers, 95 and 96 named). `strerror_r` and `perror` keep "No error: 0" and "Unknown error: %d"; they are not measured. - [strerror_s and strerrorlen_s](../../../core/libs/prx/libc/src/WideConversion.cpp) (libc) follow [C11 K.3.7.4.2 and K.3.7.4.3](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf), using the existing FreeBSD error messages and unknown-error formatting. Truncation writes an ellipsis when capacity exceeds 3; constraint failures leave the destination untouched and invoke the registered guest-ABI handler. The console's `RSIZE_MAX` (`SIZE_MAX >> 1` here), nonzero returns (`EINVAL` 22 for constraints, `ERANGE` 34 for truncation), handler arguments and default abort behavior are unverified. Messages remain in the C locale, and guest errno is preserved. - [getw and putw](../../../core/libs/prx/libc/src/BinaryWordIo.cpp) (libc) follow FreeBSD 11.4 [getw.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/getw.c) and [putw.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/putw.c): one 32-bit little-endian word, `getw` returns -1 for EOF or an error, and `putw` returns 0 on success or -1 on error while holding the stream lock. They use the existing host-backed streams, so buffering and error timing follow the host CRT. Null streams throw and closed streams return -1 with `EBADF`, as existing byte I/O does. Console behavior is unverified. From befe345441aeefe22495f8fdc4ac1a243bf2e666 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Thu, 22 Oct 2026 02:20:19 +0700 Subject: [PATCH 207/428] docs(agc): correct multisampled DCC acceptance in the CB passes debt entry Co-Authored-By: Claude --- docs/dev/debt/libSceAgcDriver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 63766b2d7e..eea65d2742 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -172,7 +172,7 @@ - [Multisampled color fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target's samples are on the host, so a fill of its color memory is applied at its next use: a uniform fill over the whole of it clears the host image to that value, and a pending CMASK fast clear takes precedence over the fill and over a fill that would throw. Its color memory is a single mip and slice of addrlib's block dimensions for its sample count (ComputeThinBlockDimension). A fill over part of it, a fill reaching the start of a target whose footprint is not computed (linear), and a fill value with no exact clear value in the format (sRGB channels other than 0 and 1) throw at the next use. A fill before the target's first use is not seen (the host image starts at 0), nor are copies or shader writes into its color memory. A uniform compute fill over exactly the whole color memory with an exact clear value is not stored into guest memory, since only the host image holds the samples. A target is retired only when its whole guarded range is unmapped or its allocation removed, so if a title reuses that memory for other data while it stays mapped, a read the guard does not check (a buffer read through a V# or a pointer) sees the bytes from before the fill. - [Multisampled texture no draw rendered to](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - sampling a multisampled texture whose surface no draw rendered to creates the host target from the T# and clears it to the color a single 32-bit value repeated over the whole footprint encodes, padding up to whole tile blocks included. Inferred, not measured on the console: that the samples of such a surface are the guest color memory, as a title that clears its canvas on the CPU (ps5-opengl `ps5_clear_msaa4_color`) relies on. Memory that is not one repeated value, tile modes other than Standard4KB, Standard64KB, Z64KBX, D64KBX and RenderTarget, unmapped memory and values with no exact clear color throw. CPU writes to the surface after its host target exists are not seen by the host. - [Sample 0 of a multisampled color target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/FirstSampleCopy.cpp) (libSceAgcDriver) - a 2D image read of a multisampled color target returns its float, UNORM, SNORM or sRGB sample 0 exactly only where the driver keeps those values through a shader and a color attachment, which Vulkan does not require, and one of a multisampled depth surface throws. The read binds a one-sample copy that a fragment pass (`texelFetch` of sample 0) draws before every such binding; integer formats copy exactly. -- [Multisampled targets in CB passes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve and an FMASK decompress (`CB_COLOR_CONTROL` mode 5) are modeled. The FMASK decompress only applies the pending CMASK fast clear to the host image; it requires a CMASK address, and the target's DCC and FMASK contents are not interpreted. DCC over a multisampled target is accepted only in that pass and throws everywhere else. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. +- [Multisampled targets in CB passes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve and an FMASK decompress (`CB_COLOR_CONTROL` mode 5) are modeled. The FMASK decompress only applies the pending CMASK fast clear to the host image; it requires a CMASK address, and the target's FMASK contents are not interpreted. In that pass as elsewhere, multisampled DCC is accepted only as a native multisampled DCC fast clear or comp-to-single clear. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. - [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, index buffer read, fixed-function vertex buffer read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB blocks for the RenderTarget, Z64KBX and D64KBX tile modes. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Live targets remain cached per device, surface address, format, extent and sample count. Retired cache owners are removed on the next target, sampled or storage lookup; outstanding recorded work and sampled views retain their images. Ordinary sampled and storage bindings check the color-target guard before resource reuse, including precollected sampled lookups. Reused image tables do not invalidate on multisample color-target registration yet; their per-element fault handling and native multisample sampling require a separate reuse proof. - [DB depth copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with `DB_RENDER_CONTROL.DEPTH_COPY` is run as a compute pass that copies one sample of the `Z_32_FLOAT` depth surface into a single-sample R32 float color target over the whole surface. With `COPY_CENTROID` the sample is the first one the draw covers, which is sample 0 under the full `PA_SC_AA_MASK` the driver requires: [radeonsi copies one sample per draw with `COPY_CENTROID` and a one-sample mask](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state.c#L1679-1681); the console is not measured. Stencil copies, depth tests, other depth formats, multisampled or other-format targets and copies over part of the surface throw. - [CB resolve sample count](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with CB_COLOR_CONTROL in resolve mode is a CB resolve when either PA_SC_AA_CONFIG or CB_COLOR0_ATTRIB.NUM_SAMPLES is multisampled, and takes its sample count from CB0. Inferred, not from documentation: PPSA36082 resolves its 4x targets with no sample count in PA_SC_AA_CONFIG, and radeonsi sets PA_SC_AA_CONFIG to 0 for a multisampled framebuffer when rasterizer multisampling and line smoothing are off. PA_SC_AA_CONFIG is only compared with CB0 while PA_SC_MODE_CNTL_0.MSAA_ENABLE is set: PPSA08270 resolves a 4x 512x512 target with MSAA_ENABLE off and PA_SC_AA_CONFIG still at 2x (0x00108001) from earlier draws. From d3525d37cd6b3013b8a4122dbf56fcaca16fda5a Mon Sep 17 00:00:00 2001 From: hypermist Date: Sun, 25 Oct 2026 01:29:12 +1300 Subject: [PATCH 208/428] fix(libSceFont): check a null outline profile before the font-set path A valid font-set handle with a null output now returns SCE_FONT_ERROR_INVALID_PARAMETER instead of throwing, matching the handle, then output, order of the firmware. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceFont/src/Render.cpp | 1 + core/libs/tests/GuestFontOutlineProfile.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/core/libs/prx/libSceFont/src/Render.cpp b/core/libs/prx/libSceFont/src/Render.cpp index 705b0c0628..4799059e1b 100644 --- a/core/libs/prx/libSceFont/src/Render.cpp +++ b/core/libs/prx/libSceFont/src/Render.cpp @@ -203,6 +203,7 @@ int APS5_VABI sceFontGetFontGlyphsOutlineProfile(FontHandle fontHandle, FontOutl std::uint32_t fontLock = 0; int rc = SCE_FONT_ERROR_INVALID_FONT_HANDLE; if (font && font->magic == HANDLE_MAGIC) { + if (!profile) return SCE_FONT_ERROR_INVALID_PARAMETER; if (font->open_info.fontset_record) NotImplemented_nid_no_patch(__func__); if (AcquireFontLock(font, fontLock)) { rc = ComputeFontGlyphsOutlineProfile(fontHandle, profile); diff --git a/core/libs/tests/GuestFontOutlineProfile.cpp b/core/libs/tests/GuestFontOutlineProfile.cpp index ba82b0d6fb..2cd7b4c8e9 100644 --- a/core/libs/tests/GuestFontOutlineProfile.cpp +++ b/core/libs/tests/GuestFontOutlineProfile.cpp @@ -101,6 +101,7 @@ int main() { std::array record{}; auto* nativeFont = reinterpret_cast(profiled); nativeFont->open_info.fontset_record = record.data(); + Require(sceFontGetFontGlyphsOutlineProfile(profiled, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); bool recordThrew = false; try { sceFontGetFontGlyphsOutlineProfile(profiled, &profile); From 2d35e00af21979118881ac4ad1bd02dd0b929f29 Mon Sep 17 00:00:00 2001 From: Emanuele <118837721+imellag@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:43:12 +0200 Subject: [PATCH 209/428] test(agc): skip cull distance cases on devices without shaderCullDistance The driver refuses a draw that enables a cull distance when the device has no shaderCullDistance, and agc_driver_vertex_clip_cull_distances, agc_driver_vertex_clip_cull_draw (with and without the draw key) and agc_driver_vertex_user_data_tail ran their cull distance cases anyway, so on MoltenVK all four failed at that refusal. They now skip only those cases when the target lacks CapabilityCullDistance, as they already skip the mesh case without VK_EXT_mesh_shader, and the shader debt entry records the refusal. --- .../tests/execution/VertexClipCullDistances.cpp | 6 ++++++ .../tests/execution/VertexClipCullDraw.cpp | 5 +++++ .../tests/execution/VertexUserDataTail.cpp | 14 ++++++++++---- docs/dev/debt/shader.md | 2 +- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp index 08adb9fde4..23c82c25eb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp @@ -18,6 +18,7 @@ using ShaderRecompiler::ShaderStage; constexpr std::uint32_t Width = 192; constexpr std::uint32_t Height = 128; constexpr std::uint32_t DistanceVector = 0x00400000u; +constexpr std::uint32_t CullDistanceEnables = 0x0000ff00u; alignas(256) std::array Pixels{}; alignas(256) constexpr std::array VertexCode{ @@ -185,14 +186,19 @@ int main() { std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); return VulkanTestSkipped; } + const bool cullDistance = TargetHasCapability(device->Target(), spv::CapabilityCullDistance); + if (!cullDistance) std::puts("cull distance cases skipped, the device has no shaderCullDistance"); + const auto runs = [&](const Row& row) { return cullDistance || (row.control & CullDistanceEnables) == 0u; }; for (const auto waveSize : {64u, 32u}) { for (const auto& row : Rows) { + if (!runs(row)) continue; Draw(*device, waveSize, row, false); Check(row, waveSize, false); } } if (device->Target().mesh.has_value()) { for (const auto& row : Rows) { + if (!runs(row)) continue; Draw(*device, 64u, row, true); Check(row, 64u, true); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDraw.cpp index 076e006c49..b4d62cdc54 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDraw.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDraw.cpp @@ -33,6 +33,7 @@ using AgcDriver::Graphics::Require; constexpr std::uint32_t Width = 192; constexpr std::uint32_t Height = 128; constexpr std::uint32_t DistanceVector = 0x00400000u; +constexpr std::uint32_t CullDistanceEnables = 0x0000ff00u; constexpr std::uint32_t SetContextReg = 0x69u; constexpr std::uint32_t SetShReg = 0x76u; constexpr std::uint32_t SetUconfigReg = 0x79u; @@ -212,14 +213,18 @@ int main(int argc, char** argv) { const bool noKey = argc > 1 && std::string(argv[1]) == "no-key"; if (noKey) Require(std::getenv("APS5_NO_DRAW_KEY") != nullptr, "the no-key run must start with APS5_NO_DRAW_KEY set"); PositionExportLayoutTests(); + bool cullDistance = false; { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + cullDistance = TargetHasCapability(device->Target(), spv::CapabilityCullDistance); } + if (!cullDistance) std::puts("cull distance steps skipped, the device has no shaderCullDistance"); ColorMemory = AllocateWatched(2 * Block); Register(VertexShader, VertexCode.data(), sizeof(VertexCode), 2); Register(PixelShader, PixelCode.data(), sizeof(PixelCode), 1); for (const auto& step : Steps) { + if (!cullDistance && (step.control & CullDistanceEnables) != 0u) continue; std::array vertices{}; for (std::size_t i = 0; i < vertices.size(); ++i) { vertices[i] = {{PositionX[i], PositionY[i], 0.5f, 1.0f}, {step.component0[i], step.component1[i], step.component2[i], 0.0f}}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexUserDataTail.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexUserDataTail.cpp index df9e6f2c6d..2130d6f232 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VertexUserDataTail.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexUserDataTail.cpp @@ -125,7 +125,7 @@ void ControlStateDraws(std::vector prefix) { CheckBranchColors(activeExec, emptyExec, "EXEC"); } -void VertexTailDraws() { +void VertexTailDraws(bool cullDistance) { std::vector prefix{ 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, 0x7e060280u, 0x7e0802f2u, 0xbe8c03ffu, 0x3f800000u, 0xbf060c0cu, @@ -142,8 +142,12 @@ void VertexTailDraws() { alignas(256) const std::array clipContinuation{0xbf840005u, 0x7e08020cu, 0xf80000cfu, 0x04030201u, 0xf80008dfu, 0x01010101u, 0xbf810000u}; const auto clipped = Check(prefix, clipContinuation, 0x00400001u, clipContinuation.size()); Require(clipped != original && std::count(clipped.begin(), clipped.end(), Kept) > kept, "a clip distance exported by the continuation did not clip the triangle"); - const auto culled = Check(prefix, clipContinuation, 0x00400100u, clipContinuation.size()); - Require(culled == original, "a mixed-sign cull distance discarded the triangle"); + if (cullDistance) { + const auto culled = Check(prefix, clipContinuation, 0x00400100u, clipContinuation.size()); + Require(culled == original, "a mixed-sign cull distance discarded the triangle"); + } else { + std::puts("cull distance draw skipped, the device has no shaderCullDistance"); + } ControlStateDraws(prefix); } @@ -151,11 +155,13 @@ void VertexTailDraws() { int main() { try { + bool cullDistance = false; { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + cullDistance = TargetHasCapability(device->Target(), spv::CapabilityCullDistance); } - VertexTailDraws(); + VertexTailDraws(cullDistance); AgcDriverShutdown_nid_postfix(); std::puts("vertex user-data tail tests passed"); return 0; diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 96d084d937..8e7ef7ab81 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -22,7 +22,7 @@ - [s_subvector_loop_begin and s_subvector_loop_end](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (core/shader/recompiler) - follow AMD's RDNA 2 instruction set reference and a gfx1036 (wave64) measurement, pinned in [SubvectorLoop.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp); `S0` in the end instruction's `elif(S0 == 0)` is read as its SGPR operand. A wave32 shader and a destination other than an SGPR throw because neither is measured. The begin instruction tests for an empty mask as `(exec_lo | exec_hi) == 0`: AMD's Windows driver (32.0.21045.5002 and 32.0.21042.62) compiled `exec_lo == 0 && exec_hi == 0` as a test of the whole 64-bit mask when guest loads use selects, as they do with `robustBufferAccess2` (#7376, #7623). Measured on PC RDNA2 (Radeon 610M), not on a PS5. The [specialization pass](../../../core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp) keeps a constant-condition branch whose untaken target is a loop header, since folding it removes the loop's back edge, which SPIR-V requires exactly once per loop header (SPIRV-Tools rejects the module and the NVIDIA driver 615.78.08 crashes compiling it); a loop whose back-edge block becomes unreachable in another way is not handled - [Vertex entry merged wave info](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) - `s3` of every vertex-path shader is set to `waveSize | waveSize << 8` (vertex and primitive count of a full wave, wave index 0), or to `32 | 32 << 8` for a wave64 program on a 32-lane host subgroup, which runs it at one lane per invocation and holds no lanes 32-63. On a device whose `subgroupSupportedStages` lacks the vertex stage (lavapipe, MoltenVK), vertex and local programs run as one-lane waves: `s3` is `1 | 1 << 8`, the lane id is 0, a ballot is the invocation's own bit, a shuffle or broadcast returns the invocation's value, an elect is true and a subgroup reduction is the value itself, so no `GroupNonUniform` capability is declared. Layout taken from the prologue Mesa's ACO emits for NGG vertex shaders (`s_bfe_u64 exec` from `s3`), not measured on hardware. The driver gives every graphics program user data from `s8`, so the condition does not tell NGG from legacy vertex shaders: they just never read `s3`. A draw whose last wave is partial still reports a full wave, and so does a host wave with idle invocations (an RTX 5070 ran every vertex warp of a non-indexed triangle list with 30 of its 32 invocations). - [s_barrier in vertex-path programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (recompiler) - emits no instruction when the program has no LDS or GDS access and no memory write or atomic, so no wave of the subgroup can observe another, or when the barrier runs before every memory access of the program (it is on the entry's straight-line path with no load, store, atomic, image or LDS access ahead of it), so it orders nothing; the `GS_ALLOC_REQ` such a barrier orders on NGG passthrough programs is already dropped on the vertex path. The Vertex execution model has no workgroup barrier. Other programs with LDS, GDS or memory writes throw. Not measured on hardware -- [Vertex clip and cull distances](../../../core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp) (libSceAgcDriver) - `PA_CL_VS_OUT_CNTL` enable bit `s` of `CLIP_DIST_ENA`/`CULL_DIST_ENA` is taken to select component `s` of the `pos1`/`pos2` exports, with the Vulkan array index counted by the enabled bits below it, and `VS_OUT_CCDIST0_VEC_ENA` as bit 22; this follows the recompiler's existing position-export mapping, not hardware or documentation. The guest clip distances share one `gl_ClipDistance` array with the all-zero clip position distance, which takes the index after them. A vertex program whose own code has no `pos1`..`pos3` export is compiled and prepared without these bits (`PositionExportLayout`), so registration and draws agree on its artifact; an export in a subroutine it calls is not seen by that scan +- [Vertex clip and cull distances](../../../core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp) (libSceAgcDriver) - `PA_CL_VS_OUT_CNTL` enable bit `s` of `CLIP_DIST_ENA`/`CULL_DIST_ENA` is taken to select component `s` of the `pos1`/`pos2` exports, with the Vulkan array index counted by the enabled bits below it, and `VS_OUT_CCDIST0_VEC_ENA` as bit 22; this follows the recompiler's existing position-export mapping, not hardware or documentation. The guest clip distances share one `gl_ClipDistance` array with the all-zero clip position distance, which takes the index after them. A vertex program whose own code has no `pos1`..`pos3` export is compiled and prepared without these bits (`PositionExportLayout`), so registration and draws agree on its artifact; an export in a subroutine it calls is not seen by that scan A device without `shaderCullDistance` (MoltenVK) throws on a draw that enables a cull distance; none is emulated. - [32_AR color exports](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - with `SPI_SHADER_COL_FORMAT` 3 the red value is export channel 0 and the alpha is export channel 1, the GFX10 layout of Mesa's [`emit_ps_color_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L400-L411), ACO and radeonsi in Mesa 22.0 ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/amd/compiler/aco_instruction_selection.cpp#L11087-L11096), [si_shader_llvm_ps.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/gallium/drivers/radeonsi/si_shader_llvm_ps.c#L323-L333)) and AMD's [LLPC](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/FragmentColorExport.cpp#L157-L168); before GFX10 the alpha was channel 3. Channels 2 and 3, which none of these compilers enables, are ignored; green and blue are 0 and the alpha of an export without channel 1 is 1.0, the defaults of the other formats, which no source gives. Not measured on hardware, neither on gfx1013 nor on a PC RDNA 2 GPU; pinned in [Pixel32ArExport.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/Pixel32ArExport.cpp) - [GCN instructions](../../../core/shader/recompiler/RdnaDecoder/src/GcnInstructionDecoder.cpp) (shader, PS4) - PS4 (Sea Islands) instructions are re-encoded to their RDNA encodings with opcode tables built from shadPS4's `opcodes.h` and decoded by the RDNA decoder; an encoding without an RDNA counterpart throws. MIMG has no DIM field on GCN: the dimension is taken as 2D, or 2D array when DA is set, and the [materializer](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) rejects a T# whose type needs another address count. MIMG word 1 bits [31:26] are reserved on GCN (A16 and D16 on RDNA). - [GCN buffer and image descriptors](../../../core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp) (shader, PS4) - a GCN V# word 3 (DATA_FORMAT [18:15], NUM_FORMAT [14:12], names read from the most significant field) is rewritten to the unified format of the PS5 layout with OOB_SELECT 1 for a strided buffer and 3 otherwise, following the GFX6-7 NUM_RECORDS rules in Mesa's `ac_descriptors`; ATC, HASH, HEAP and MTYPE are dropped. The V# base is 44 bits, as in shadPS4's `AmdGpu::Buffer`: Limbo leaves 0x7 in word 1 bits [15:12]. A GCN T# is rewritten to the PS5 layout with SW_MODE linear, keeping TILING_INDEX [4:0], POW2_PAD [5] and PITCH - 1 [19:6] in word 7. None of it is checked on a console. From 0a8503d73d1e7b4d589ac7e47e50963193072361 Mon Sep 17 00:00:00 2001 From: Emanuele <118837721+imellag@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:53:25 +0200 Subject: [PATCH 210/428] docs(shader): end the clip and cull distance debt sentence before the cull distance note --- docs/dev/debt/shader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 8e7ef7ab81..a8254863ea 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -22,7 +22,7 @@ - [s_subvector_loop_begin and s_subvector_loop_end](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (core/shader/recompiler) - follow AMD's RDNA 2 instruction set reference and a gfx1036 (wave64) measurement, pinned in [SubvectorLoop.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp); `S0` in the end instruction's `elif(S0 == 0)` is read as its SGPR operand. A wave32 shader and a destination other than an SGPR throw because neither is measured. The begin instruction tests for an empty mask as `(exec_lo | exec_hi) == 0`: AMD's Windows driver (32.0.21045.5002 and 32.0.21042.62) compiled `exec_lo == 0 && exec_hi == 0` as a test of the whole 64-bit mask when guest loads use selects, as they do with `robustBufferAccess2` (#7376, #7623). Measured on PC RDNA2 (Radeon 610M), not on a PS5. The [specialization pass](../../../core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp) keeps a constant-condition branch whose untaken target is a loop header, since folding it removes the loop's back edge, which SPIR-V requires exactly once per loop header (SPIRV-Tools rejects the module and the NVIDIA driver 615.78.08 crashes compiling it); a loop whose back-edge block becomes unreachable in another way is not handled - [Vertex entry merged wave info](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) - `s3` of every vertex-path shader is set to `waveSize | waveSize << 8` (vertex and primitive count of a full wave, wave index 0), or to `32 | 32 << 8` for a wave64 program on a 32-lane host subgroup, which runs it at one lane per invocation and holds no lanes 32-63. On a device whose `subgroupSupportedStages` lacks the vertex stage (lavapipe, MoltenVK), vertex and local programs run as one-lane waves: `s3` is `1 | 1 << 8`, the lane id is 0, a ballot is the invocation's own bit, a shuffle or broadcast returns the invocation's value, an elect is true and a subgroup reduction is the value itself, so no `GroupNonUniform` capability is declared. Layout taken from the prologue Mesa's ACO emits for NGG vertex shaders (`s_bfe_u64 exec` from `s3`), not measured on hardware. The driver gives every graphics program user data from `s8`, so the condition does not tell NGG from legacy vertex shaders: they just never read `s3`. A draw whose last wave is partial still reports a full wave, and so does a host wave with idle invocations (an RTX 5070 ran every vertex warp of a non-indexed triangle list with 30 of its 32 invocations). - [s_barrier in vertex-path programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (recompiler) - emits no instruction when the program has no LDS or GDS access and no memory write or atomic, so no wave of the subgroup can observe another, or when the barrier runs before every memory access of the program (it is on the entry's straight-line path with no load, store, atomic, image or LDS access ahead of it), so it orders nothing; the `GS_ALLOC_REQ` such a barrier orders on NGG passthrough programs is already dropped on the vertex path. The Vertex execution model has no workgroup barrier. Other programs with LDS, GDS or memory writes throw. Not measured on hardware -- [Vertex clip and cull distances](../../../core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp) (libSceAgcDriver) - `PA_CL_VS_OUT_CNTL` enable bit `s` of `CLIP_DIST_ENA`/`CULL_DIST_ENA` is taken to select component `s` of the `pos1`/`pos2` exports, with the Vulkan array index counted by the enabled bits below it, and `VS_OUT_CCDIST0_VEC_ENA` as bit 22; this follows the recompiler's existing position-export mapping, not hardware or documentation. The guest clip distances share one `gl_ClipDistance` array with the all-zero clip position distance, which takes the index after them. A vertex program whose own code has no `pos1`..`pos3` export is compiled and prepared without these bits (`PositionExportLayout`), so registration and draws agree on its artifact; an export in a subroutine it calls is not seen by that scan A device without `shaderCullDistance` (MoltenVK) throws on a draw that enables a cull distance; none is emulated. +- [Vertex clip and cull distances](../../../core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp) (libSceAgcDriver) - `PA_CL_VS_OUT_CNTL` enable bit `s` of `CLIP_DIST_ENA`/`CULL_DIST_ENA` is taken to select component `s` of the `pos1`/`pos2` exports, with the Vulkan array index counted by the enabled bits below it, and `VS_OUT_CCDIST0_VEC_ENA` as bit 22; this follows the recompiler's existing position-export mapping, not hardware or documentation. The guest clip distances share one `gl_ClipDistance` array with the all-zero clip position distance, which takes the index after them. A vertex program whose own code has no `pos1`..`pos3` export is compiled and prepared without these bits (`PositionExportLayout`), so registration and draws agree on its artifact; an export in a subroutine it calls is not seen by that scan. A device without `shaderCullDistance` (MoltenVK) throws on a draw that enables a cull distance; none is emulated. - [32_AR color exports](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - with `SPI_SHADER_COL_FORMAT` 3 the red value is export channel 0 and the alpha is export channel 1, the GFX10 layout of Mesa's [`emit_ps_color_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L400-L411), ACO and radeonsi in Mesa 22.0 ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/amd/compiler/aco_instruction_selection.cpp#L11087-L11096), [si_shader_llvm_ps.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/gallium/drivers/radeonsi/si_shader_llvm_ps.c#L323-L333)) and AMD's [LLPC](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/FragmentColorExport.cpp#L157-L168); before GFX10 the alpha was channel 3. Channels 2 and 3, which none of these compilers enables, are ignored; green and blue are 0 and the alpha of an export without channel 1 is 1.0, the defaults of the other formats, which no source gives. Not measured on hardware, neither on gfx1013 nor on a PC RDNA 2 GPU; pinned in [Pixel32ArExport.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/Pixel32ArExport.cpp) - [GCN instructions](../../../core/shader/recompiler/RdnaDecoder/src/GcnInstructionDecoder.cpp) (shader, PS4) - PS4 (Sea Islands) instructions are re-encoded to their RDNA encodings with opcode tables built from shadPS4's `opcodes.h` and decoded by the RDNA decoder; an encoding without an RDNA counterpart throws. MIMG has no DIM field on GCN: the dimension is taken as 2D, or 2D array when DA is set, and the [materializer](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) rejects a T# whose type needs another address count. MIMG word 1 bits [31:26] are reserved on GCN (A16 and D16 on RDNA). - [GCN buffer and image descriptors](../../../core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp) (shader, PS4) - a GCN V# word 3 (DATA_FORMAT [18:15], NUM_FORMAT [14:12], names read from the most significant field) is rewritten to the unified format of the PS5 layout with OOB_SELECT 1 for a strided buffer and 3 otherwise, following the GFX6-7 NUM_RECORDS rules in Mesa's `ac_descriptors`; ATC, HASH, HEAP and MTYPE are dropped. The V# base is 44 bits, as in shadPS4's `AmdGpu::Buffer`: Limbo leaves 0x7 in word 1 bits [15:12]. A GCN T# is rewritten to the PS5 layout with SW_MODE linear, keeping TILING_INDEX [4:0], POW2_PAD [5] and PITCH - 1 [19:6] in word 7. None of it is checked on a console. From 2d32e8bd85a0d2cd916a2b5c5cebee1462eb299e Mon Sep 17 00:00:00 2001 From: Retr0 Date: Sat, 24 Oct 2026 16:00:43 +0300 Subject: [PATCH 211/428] fix(libkernel_sys): check the linkat source before resolving the target --- core/libs/prx/libkernel_sys/Export.cpp | 22 +++++++++++++++------- core/libs/tests/GuestKernelSys.cpp | 6 ++++++ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 048d457629..94c339ffac 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -436,17 +436,21 @@ static int FollowLinkPath(const std::filesystem::path& source, std::filesystem:: } #endif -static int LinkNative(const std::filesystem::path& source, const std::filesystem::path& destination) { +static int LinkSourceError(const std::filesystem::path& source) { std::error_code error; const auto status = std::filesystem::status(source, error); if (!std::filesystem::exists(status)) { #ifdef _WIN32 std::filesystem::path target; - if (const int followError = FollowLinkPath(source, target)) return static_cast(Fail(followError)); + if (const int followError = FollowLinkPath(source, target)) return followError; #endif - return static_cast(Fail(HasNonDirectoryPrefix(source) ? GuestEnotdir : GuestEnoent)); + return HasNonDirectoryPrefix(source) ? GuestEnotdir : GuestEnoent; } - if (std::filesystem::is_directory(status)) return static_cast(Fail(GuestEperm)); + return std::filesystem::is_directory(status) ? GuestEperm : 0; +} + +static int LinkNative(const std::filesystem::path& source, const std::filesystem::path& destination) { + std::error_code error; if (std::filesystem::exists(std::filesystem::symlink_status(destination, error))) return static_cast(Fail(GuestEexist)); if (!std::filesystem::is_directory(destination.parent_path(), error)) return static_cast(Fail(HasNonDirectoryPrefix(destination) ? GuestEnotdir : GuestEnoent)); #ifdef _WIN32 @@ -469,6 +473,7 @@ int APS5_VABI link_nid_postfix(const char* existing, const char* created) { std::filesystem::path source, destination; if (const int error = ResolvePathError_nid_no_patch(existing, source)) return static_cast(Fail(error)); if (const int error = ResolvePathError_nid_no_patch(created, destination)) return static_cast(Fail(error)); + if (const int error = LinkSourceError(source)) return static_cast(Fail(error)); return LinkNative(source, destination); } @@ -484,12 +489,15 @@ static int ResolveAtPath(int fd, const char* path, std::filesystem::path& native int APS5_VABI linkat_nid_postfix(int fd1, const char* path1, int fd2, const char* path2, int flag) { if (flag & ~GuestAtSymlinkFollow) return static_cast(Fail(GuestEinval)); - if (path1 == nullptr || path2 == nullptr) return static_cast(Fail(GuestEfault)); - if (*path1 == '\0' || *path2 == '\0') return static_cast(Fail(GuestEnoent)); + if (path1 == nullptr) return static_cast(Fail(GuestEfault)); + if (*path1 == '\0') return static_cast(Fail(GuestEnoent)); std::filesystem::path source, destination; if (const int error = ResolveAtPath(fd1, path1, source)) return static_cast(Fail(error)); - if (const int error = ResolveAtPath(fd2, path2, destination)) return static_cast(Fail(error)); if (flag == 0 && IsSymbolicLink(source)) NotImplemented_nid_no_patch("linkat: a hard link to a symbolic link itself"); + if (const int error = LinkSourceError(source)) return static_cast(Fail(error)); + if (path2 == nullptr) return static_cast(Fail(GuestEfault)); + if (*path2 == '\0') return static_cast(Fail(GuestEnoent)); + if (const int error = ResolveAtPath(fd2, path2, destination)) return static_cast(Fail(error)); return LinkNative(source, destination); } diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 4f5aaa08a9..dee944c4fd 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -171,6 +171,12 @@ void OwnershipAndLinkTests(const std::filesystem::path& root) { Require(linkat_nid_postfix(directory, "missing.txt", directory, "absent.txt", 0) == -1 && Error() == 2); Require(linkat_nid_postfix(4321, "owned.txt", directory, "absent.txt", 0) == -1 && Error() == 9); Require(linkat_nid_postfix(directory, "owned.txt", 4321, "absent.txt", 0) == -1 && Error() == 9); + Require(linkat_nid_postfix(directory, "missing.txt", 4321, "absent.txt", 0) == -1 && Error() == 2); + Require(linkat_nid_postfix(directory, "missing.txt", -100, nullptr, 0) == -1 && Error() == 2); + Require(linkat_nid_postfix(-100, root.string().c_str(), 4321, "absent.txt", 0) == -1 && Error() == 1); + Require(linkat_nid_postfix(-100, root.string().c_str(), -100, "", 0) == -1 && Error() == 1); + Require(linkat_nid_postfix(directory, "owned.txt", -100, nullptr, 0) == -1 && Error() == 14); + Require(linkat_nid_postfix(directory, "owned.txt", -100, "", 0) == -1 && Error() == 2); const int plain = sceKernelOpen(file.c_str(), SCE_KERNEL_O_RDONLY, 0); Require(plain >= 0); Require(linkat_nid_postfix(plain, "owned.txt", directory, "absent.txt", 0) == -1 && Error() == 20); From 8d935498a0b6d2bf9988bd3d5125c017cbf417b8 Mon Sep 17 00:00:00 2001 From: Bram910 Date: Sat, 24 Oct 2026 15:21:53 +0200 Subject: [PATCH 212/428] fix(tools): reject rewritten library ZIP paths --- tools/build_web.py | 3 ++- tools/tests/test_build_web.py | 34 +++++++++++++++++++++++++++++++++ tools/tests/test_web_release.py | 14 ++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/tools/build_web.py b/tools/build_web.py index 8a5e555bf0..74fc9c531c 100644 --- a/tools/build_web.py +++ b/tools/build_web.py @@ -71,7 +71,8 @@ def publish_libraries(output, release=None): raise ValueError("Invalid library archive") names = {} for file in files: - if not file.filename.startswith("libs/") or any(part in ("..", ".", "") for part in file.filename.rstrip("/").split("/")) or "\\" in file.filename: + if (file.orig_filename != file.filename or not file.filename.startswith("libs/") + or any(part in ("..", ".", "") for part in file.filename.rstrip("/").split("/")) or "\\" in file.filename): raise ValueError("Invalid library archive layout") name = file.filename.rstrip("/").lower() if name in names: diff --git a/tools/tests/test_build_web.py b/tools/tests/test_build_web.py index 11c38bfdd4..455da60cc9 100644 --- a/tools/tests/test_build_web.py +++ b/tools/tests/test_build_web.py @@ -26,6 +26,40 @@ def library_zip(entries): class LibraryPublicationTests(unittest.TestCase): + def test_original_archive_paths_are_rejected_before_decompression(self): + valid = library_zip([("libs/libc.prx", b"valid")]) + for name in ("libs/libc.prx\0ignored", "libs/sub\0dir/libc.prx", + "libs\\libc.prx", "libs/sub\\libc.prx"): + with self.subTest(name=name): + placeholder = name.replace("\0", "!").replace("\\", "!") + invalid = library_zip([(placeholder, b"content")]) + invalid = invalid.replace(placeholder.encode(), name.encode()) + with zipfile.ZipFile(io.BytesIO(invalid)) as archive: + self.assertEqual(archive.infolist()[0].orig_filename, name) + with patch.object(zipfile.ZipFile, "testzip", side_effect=AssertionError("archive decompressed")): + with self.assertRaisesRegex(ValueError, "Invalid library archive layout"): + self.publish(invalid, valid) + self.assertEqual(self.manifest.read_bytes(), self.previous) + self.assertEqual(list(self.libraries.iterdir()), [self.existing]) + + def test_rewritten_path_in_second_platform_preserves_previous_manifest(self): + valid = library_zip([("libs/libc.prx", b"valid")]) + invalid = library_zip([("libs/libc.prx!ignored", b"content")]) + invalid = invalid.replace(b"libs/libc.prx!ignored", b"libs/libc.prx\0ignored") + with self.assertRaisesRegex(ValueError, "Invalid library archive layout"): + self.publish(valid, invalid) + self.assertEqual(self.manifest.read_bytes(), self.previous) + self.assertEqual(self.existing.read_bytes(), b"previous archive") + self.assertFalse(any(file.suffix == ".download" for file in self.libraries.iterdir())) + + def test_unicode_archive_paths_are_published_unchanged(self): + payload = library_zip([("libs/\u65e5\u672c\u8a9e/\u00e9.prx", b"content"), + ("libs/licenses/Third party.txt", b"license")]) + self.publish(payload, payload) + manifest = json.loads(self.manifest.read_text(encoding="utf-8")) + for asset in manifest["platforms"].values(): + self.assertEqual((self.output / asset["path"]).read_bytes(), payload) + def test_expanded_limit_is_checked_before_decompression(self): payload = library_zip([("libs/libc.prx", b"a" * 4097)]) with patch.object(build_web, "MAX_EXPANDED_BYTES", 4096, create=True), \ diff --git a/tools/tests/test_web_release.py b/tools/tests/test_web_release.py index d67632e117..f46e4c683e 100644 --- a/tools/tests/test_web_release.py +++ b/tools/tests/test_web_release.py @@ -105,6 +105,20 @@ class WebReleaseTests(unittest.TestCase): self.assertTrue((self.output / ".nojekyll").is_file()) self.assertEqual(self.requests.count(LATEST), 1) + def test_rewritten_library_paths_preserve_the_previous_site(self): + invalid = zipped([("libs/libkernel.prx!ignored", b"content")]) + invalid = invalid.replace(b"libs/libkernel.prx!ignored", b"libs/libkernel.prx\0ignored") + for platform in ("windows", "linux"): + with self.subTest(platform=platform): + self.requests.clear() + name = f"prx-{platform}-v1.zip" + original = self.responses["https://example.test/" + name] + self.set_asset(name, invalid) + try: + self.assert_rejected("Invalid library archive layout") + finally: + self.set_asset(name, original) + def test_libraries_flag_also_replaces_an_incompatible_local_converter(self): self.seed_previous() source = self.root / "source/demo/web" From 65b261b9de90b9089843d1693b8c0dc40ebeb308 Mon Sep 17 00:00:00 2001 From: noir Date: Sat, 24 Oct 2026 15:33:12 +0200 Subject: [PATCH 213/428] fix(agc): build the pipeline journal replay at a lower thread priority The replay built its pipelines on half the CPU threads at the title's priority; next to a title whose job threads spin (Demon's Souls) they left the presenter and the queue workers without CPU time, and with the fifo present mode the title stopped after two presents (#7203). The replay threads now run below normal priority (Windows) or at nice + 5 (Linux). Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/PipelineJournal.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index 0b0f4d6068..3bdc618855 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -21,8 +21,10 @@ #ifdef _WIN32 #include #else +#include #include #include +#include #include #endif @@ -1215,6 +1217,17 @@ ReplayCounters& replayCounters() { return counters; } +void lowerReplayThreadPriority() { +#ifdef _WIN32 + if (!SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL)) throw std::system_error(static_cast(GetLastError()), std::system_category(), "pipeline journal replay thread priority"); +#elif defined(__linux__) + errno = 0; + const int nice = getpriority(PRIO_PROCESS, static_cast(gettid())); + if (nice == -1 && errno != 0) throw std::system_error(errno, std::generic_category(), "pipeline journal replay thread priority"); + if (setpriority(PRIO_PROCESS, static_cast(gettid()), std::min(nice + 5, 19)) != 0) throw std::system_error(errno, std::generic_category(), "pipeline journal replay thread priority"); +#endif +} + // Builds the journal's pipelines for one device on a pool: the first thread loads the file, every // thread then takes the next entry. A pipeline the store already holds counts as present; an entry // whose modules are missing or whose set layouts differ from what its modules describe is rejected. @@ -1248,6 +1261,11 @@ public: private: void run() { + try { + lowerReplayThreadPriority(); + } catch (const std::exception& error) { + std::fprintf(stderr, "[pipeline-journal] %s\n", error.what()); + } std::call_once(loadOnce, [&] { try { auto journal = Journal::Instance().Load(); From dfc705eb41073230f56cb89a332eabee2a9bc5c5 Mon Sep 17 00:00:00 2001 From: cocoa55 <284489448+cocoa55@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:29:45 -0400 Subject: [PATCH 214/428] fix(codegen): reject branches into a rewritten instruction --- .../codegen/src/x86/X64InstructionRewriter.cpp | 6 ++++++ core/relinker/codegen/tests/RewriteBoundaryTests.cpp | 12 +++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/core/relinker/codegen/src/x86/X64InstructionRewriter.cpp b/core/relinker/codegen/src/x86/X64InstructionRewriter.cpp index 82990dd9ac..e17dde61e9 100644 --- a/core/relinker/codegen/src/x86/X64InstructionRewriter.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionRewriter.cpp @@ -54,6 +54,12 @@ std::int64_t X64InstructionRewriter::AdjustedDelta( std::int64_t sliceSize, std::int64_t delta ) { + const bool targetInsideCut = targetPosition > cutStart && targetPosition < cutEnd; + + if (targetInsideCut) { + throw CodegenException("Branch target inside rewritten instruction", static_cast(instrPosition)); + } + const bool instrShifts = instrPosition >= cutEnd && instrPosition < sliceSize; const bool targetShifts = targetPosition >= cutEnd && targetPosition < sliceSize; diff --git a/core/relinker/codegen/tests/RewriteBoundaryTests.cpp b/core/relinker/codegen/tests/RewriteBoundaryTests.cpp index dd1bd9a185..66a6ed2fc6 100644 --- a/core/relinker/codegen/tests/RewriteBoundaryTests.cpp +++ b/core/relinker/codegen/tests/RewriteBoundaryTests.cpp @@ -54,6 +54,16 @@ int main() { Require(result.Bytes == std::vector{ 0x90, 0x90, 0x90, 0xC3}); + bool targetInsideCutRejected = false; + try { + (void)rewriter.Rewrite( + {0xEB, 0x01, 0xB8, 0x11, 0x22, 0x33, 0x44, 0xC3}, + {2, {0x90}}); + } catch (const std::exception&) { + targetInsideCutRejected = true; + } + Require(targetInsideCutRejected); + for (const auto& code : { std::vector{ 0x66, 0xEB, 0x01, 0x90, 0xC3}, @@ -107,7 +117,7 @@ int main() { } RequireTruncatedBranchRejected(backwardWraparoundRejected); - std::cout << "21 rewrite boundary cases passed\n"; + std::cout << "22 rewrite boundary cases passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; From 21d956616667d96ae524381df591f4e5277c08d0 Mon Sep 17 00:00:00 2001 From: noir Date: Sat, 24 Oct 2026 15:55:22 +0200 Subject: [PATCH 215/428] fix(agc): keep the replay priority change to Windows The Linux path was not built or run; Linux keeps the replay threads at their priority. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/PipelineJournal.cpp | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index 3bdc618855..e584ea0970 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -21,10 +21,8 @@ #ifdef _WIN32 #include #else -#include #include #include -#include #include #endif @@ -1217,17 +1215,6 @@ ReplayCounters& replayCounters() { return counters; } -void lowerReplayThreadPriority() { -#ifdef _WIN32 - if (!SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL)) throw std::system_error(static_cast(GetLastError()), std::system_category(), "pipeline journal replay thread priority"); -#elif defined(__linux__) - errno = 0; - const int nice = getpriority(PRIO_PROCESS, static_cast(gettid())); - if (nice == -1 && errno != 0) throw std::system_error(errno, std::generic_category(), "pipeline journal replay thread priority"); - if (setpriority(PRIO_PROCESS, static_cast(gettid()), std::min(nice + 5, 19)) != 0) throw std::system_error(errno, std::generic_category(), "pipeline journal replay thread priority"); -#endif -} - // Builds the journal's pipelines for one device on a pool: the first thread loads the file, every // thread then takes the next entry. A pipeline the store already holds counts as present; an entry // whose modules are missing or whose set layouts differ from what its modules describe is rejected. @@ -1261,11 +1248,9 @@ public: private: void run() { - try { - lowerReplayThreadPriority(); - } catch (const std::exception& error) { - std::fprintf(stderr, "[pipeline-journal] %s\n", error.what()); - } +#ifdef _WIN32 + if (!SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL)) std::fprintf(stderr, "[pipeline-journal] replay thread priority not lowered (error %lu)\n", GetLastError()); +#endif std::call_once(loadOnce, [&] { try { auto journal = Journal::Instance().Load(); From 28f3f438427d920c7ccc451adfc004ed0ced4f7d Mon Sep 17 00:00:00 2001 From: Maximilian Fleckenstein <166870690+maximilianfv@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:05:52 -0300 Subject: [PATCH 216/428] fix(tests): give each RTC test run its own title directory EnterTitleDirectory named the run's directory after the current steady_clock value. guest_rtc and guest_rtc_ps5_title run the same test binary, and when ctest starts both at once they can read the same value and share one directory; guest_rtc then writes its PS4 title there and the PS5 run reads it and aborts. Name the directory with std::random_device instead, as the other tests that make temporary directories do. Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestRtc.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 09d0659f5f..5761ac8202 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -15,6 +14,7 @@ #include #include #include +#include extern "C" { int APS5_VABI sceRtcCheckValid(const RtcDateTime*); @@ -126,7 +126,7 @@ static void SetCompiledSdkVersion(std::byte* process, std::uint32_t version) { } static void EnterTitleDirectory(const char* titleId) { - const auto directory = std::filesystem::temp_directory_path() / ("rtc-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + const auto directory = std::filesystem::temp_directory_path() / ("rtc-test-" + std::to_string(std::random_device{}())); std::filesystem::create_directories(directory / "app0" / "sce_sys"); std::filesystem::current_path(directory); if (titleId == nullptr) return; From ae83030621f1b97ed39b6374869c61bd07a6c352 Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Sat, 24 Oct 2026 16:15:57 +0200 Subject: [PATCH 217/428] ci(windows): keep test temporary files on the runner's work disk On windows-latest, TEMP is on C:, which writes 256 MiB in 1.9 s against 0.2 s on D:, where RUNNER_TEMP and the workspace live. The ctest step now points TEMP and TMP at runner.temp. On a probe runner the optional byte-cap capture test drops from a 6.4 s median to 1.3 s with four copies running, and the git-heavy tool tests from 26-35 s to 24-26 s. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3dbe96d99d..acb6a09aa5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -683,6 +683,9 @@ jobs: shell: cmd run: cmake --build build --target libs - shell: cmd + env: + TEMP: ${{ runner.temp }} + TMP: ${{ runner.temp }} run: python tools/select_tests.py ctest build --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log --output-junit ctest.xml - run: python tools/check_skips.py build/ctest.xml --job windows - name: Write the test graph of main From f1a7f913c726b782dcdaf11fb496fd3915b80eb3 Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:38:52 +0200 Subject: [PATCH 218/428] fix(relinker): reject executable exports outside PT_LOAD and PT_TLS memory An exported symbol of the executable must lie inside a PT_LOAD segment, executable for STT_FUNC, together with its size and at least one byte. A non-TLS export in SHN_ABS is exempt from the range check, and any other special section index is rejected. An STT_TLS export needs a regular section index and a PT_TLS header and must lie inside it. GuestImageReader applies the PT_LOAD, PF_X and PT_TLS bounds to every symbol of a guest module, and the macOS target applied the bounds and the section rule to the executable's exports. Linux rejected only a special section index at a fixed base, and Windows only an STT_FUNC export with a nonzero size outside the code, in code analysis; every other export was copied into the output unchecked. The check now runs in the pipeline, so every target reports the same error for an export it rejects, and the macOS export test expects that message. --- core/relinker/CMakeLists.txt | 2 + .../src/pipeline/RelinkerPipeline.cpp | 39 +++++- .../tests/test_executable_export_bounds.py | 129 ++++++++++++++++++ .../tests/test_macos_executable_exports.py | 6 +- 4 files changed, 168 insertions(+), 8 deletions(-) create mode 100644 core/relinker/relinker/tests/test_executable_export_bounds.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 97e979dd21..f8f47ed591 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -565,6 +565,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_executable_exports.py $) add_test(NAME executable_tls_relocations COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_executable_tls_relocations.py $) + add_test(NAME executable_export_bounds COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_executable_export_bounds.py $) add_test(NAME macos_empty_tls COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_empty_tls.py $) set_tests_properties(macos_empty_tls PROPERTIES TIMEOUT 60) diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 77044446d2..0e5fdc4b79 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -434,13 +434,42 @@ RelinkResult RelinkerPipeline::Relink() { std::memcpy(&sourceName, symbol, sizeof(sourceName)); auto name = readCStr(sourceName); name.resize(name.find('#') == std::string::npos ? name.size() : name.find('#')); + const FileByteOffset entryOffset = dynSymTabOffset + off; + const auto type = symbol[4] & 15; + std::uint64_t value = 0; + std::uint64_t size = 0; + std::memcpy(&value, symbol + 8, sizeof(value)); + std::memcpy(&size, symbol + 16, sizeof(size)); + if (type == STT_TLS) { + if (section >= SHN_LORESERVE) + throw RelinkerException("Unsupported executable TLS export section: " + name, entryOffset); + const auto tls = std::find_if(programHeaders.begin(), programHeaders.end(), + [](const ProgramHeader& ph) { return ph.Type == PT_TLS; }); + if (tls == programHeaders.end()) + throw RelinkerException("Executable TLS export has no PT_TLS segment: " + name, entryOffset); + if (value > tls->MemorySize || size > tls->MemorySize - value) + throw RelinkerException("Executable TLS export exceeds PT_TLS: " + name, entryOffset); + } else if (section != SHN_ABS) { + if (section >= SHN_LORESERVE) + throw RelinkerException("Unsupported executable export section: " + name, entryOffset); + const std::uint32_t flags = type == STT_FUNC ? PF_X : 0; + const auto extent = std::max(size, 1); + const bool mapped = std::any_of(programHeaders.begin(), programHeaders.end(), [&](const ProgramHeader& ph) { + return ph.Type == PT_LOAD && (ph.Flags & flags) == flags && value >= ph.MappedAddress && + value - ph.MappedAddress < ph.MemorySize && extent <= ph.MemorySize - (value - ph.MappedAddress); + }); + if (!mapped) { + std::ostringstream message; + message << "Executable export " << name << " at 0x" << std::hex << value + << (size > 1 ? " (" + std::to_string(size) + " bytes) does not fit inside " : " is not inside ") + << (flags != 0 ? "an executable" : "a") << " PT_LOAD segment"; + throw RelinkerException(message.str(), entryOffset); + } + } exportedSymbols.emplace_back(off, std::move(name)); const auto binding = symbol[4] >> 4; - if ((binding == STB_GLOBAL || binding == STB_WEAK) && (symbol[4] & 15) == STT_FUNC) { - VirtualAddress address = 0; - std::memcpy(&address, symbol + 8, sizeof(address)); - exportedFunctions.push_back(address); - } + if ((binding == STB_GLOBAL || binding == STB_WEAK) && type == STT_FUNC) + exportedFunctions.push_back(value); } } diff --git a/core/relinker/relinker/tests/test_executable_export_bounds.py b/core/relinker/relinker/tests/test_executable_export_bounds.py new file mode 100644 index 0000000000..0b5ea1a71c --- /dev/null +++ b/core/relinker/relinker/tests/test_executable_export_bounds.py @@ -0,0 +1,129 @@ +"""An exported symbol of the executable must lie inside the image it is exported from, the rule +GuestImageReader applies to every symbol of a guest module and the macOS target applied to the executable +alone. A defined symbol that is not local, hidden or internal must lie inside a PT_LOAD segment that is +executable for STT_FUNC, together with its size and at least one byte; a non-TLS export in SHN_ABS is exempt, +any other special section index is rejected, and an STT_TLS export must lie inside PT_TLS with a regular +section index. Symbols that are not exports are not checked. The check runs before any target-specific code, +so an export it rejects fails with the same line, at the symbol entry's file offset, on every target.""" + +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_linux_load_alignment import fixture + +PT_LOAD = 1 +PT_TLS = 7 +NOTYPE, OBJECT, FUNC, TLS = 0, 1, 2, 6 +LOCAL, GLOBAL, WEAK = 0, 1, 2 +SHN_ABS = 0xfff1 +SHN_SPECIAL = 0xff01 +TARGETS = {(): "linux", ("--lazy-binding",): "lazy", ("--windows",): "windows", ("--macos",): "macos"} +SYMBOL_OFFSET = 0x4900 + 24 +TLS_MEMSZ, TLS_ALIGN = 0x24, 16 + + +def case(name, kind, value, size, message=None, binding=GLOBAL, section=1, visibility=0, tls=False): + return name, kind, value, size, message, binding, section, visibility, tls + + +CASES = [ + case("func_in_text", FUNC, 0x200, 6), + case("object_in_data", OBJECT, 0x1100, 8), + case("object_fills_load_end", OBJECT, 0x1ff8, 8), + case("notype_zero_size_inside", NOTYPE, 0x1ff0, 0), + case("func_last_byte", FUNC, 0xfff, 0), + case("weak_object", OBJECT, 0x1100, 8, binding=WEAK), + case("absolute", OBJECT, 0x123456789, 8, section=SHN_ABS), + case("tls_inside", TLS, 0x10, 4, tls=True), + case("hidden_outside_ignored", OBJECT, 0x9000, 8, visibility=2), + case("local_outside_ignored", OBJECT, 0x9000, 8, binding=LOCAL), + case("internal_outside_ignored", OBJECT, 0x9000, 8, visibility=1), + case("tls_fills", TLS, 0x20, 4, tls=True), + case("func_outside", FUNC, 0x9000, 6, + "Executable export {} at 0x9000 (6 bytes) does not fit inside an executable PT_LOAD segment"), + case("object_outside", OBJECT, 0x9000, 8, + "Executable export {} at 0x9000 (8 bytes) does not fit inside a PT_LOAD segment"), + case("object_past_end", OBJECT, 0x1ffc, 8, + "Executable export {} at 0x1ffc (8 bytes) does not fit inside a PT_LOAD segment"), + case("object_straddles_text_end", OBJECT, 0xffc, 8, + "Executable export {} at 0xffc (8 bytes) does not fit inside a PT_LOAD segment"), + case("protected_outside", OBJECT, 0x9000, 8, + "Executable export {} at 0x9000 (8 bytes) does not fit inside a PT_LOAD segment", visibility=3), + case("marker_at_end", NOTYPE, 0x2000, 0, + "Executable export {} at 0x2000 is not inside a PT_LOAD segment"), + case("func_in_data", FUNC, 0x1100, 6, + "Executable export {} at 0x1100 (6 bytes) does not fit inside an executable PT_LOAD segment"), + case("func_straddles_text_end", FUNC, 0xffc, 8, + "Executable export {} at 0xffc (8 bytes) does not fit inside an executable PT_LOAD segment"), + case("func_zero_in_data", FUNC, 0x1100, 0, + "Executable export {} at 0x1100 is not inside an executable PT_LOAD segment"), + case("special_section", OBJECT, 0x1100, 8, + "Unsupported executable export section: {}", section=SHN_SPECIAL), + case("tls_beyond", TLS, 0x20, 8, + "Executable TLS export exceeds PT_TLS: {}", tls=True), + case("tls_without_segment", TLS, 0x10, 4, + "Executable TLS export has no PT_TLS segment: {}"), + case("tls_special_section", TLS, 0x10, 4, + "Unsupported executable TLS export section: {}", section=SHN_ABS, tls=True), +] + + +def image(name, kind, value, size, binding, section, visibility, tls): + strings = b"\0" + name.encode("ascii") + b"\0" + data = fixture() + struct.pack_into(" Date: Sat, 24 Oct 2026 16:07:39 +0200 Subject: [PATCH 219/428] fix(shader): resolve each shader cache source and include path once ShaderCacheVersion.cmake resolved every source's real path twice and every include's real path once per include line: 1102 REALPATH calls per run, about 1.6 ms each on Windows. Reuse the source paths from the first pass and resolve each include path once. The generated version file is unchanged. Co-Authored-By: Claude Opus 5.5 --- core/shader/recompiler/ShaderCacheVersion.cmake | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/shader/recompiler/ShaderCacheVersion.cmake b/core/shader/recompiler/ShaderCacheVersion.cmake index f4477ed1e1..b3cbea2c75 100644 --- a/core/shader/recompiler/ShaderCacheVersion.cmake +++ b/core/shader/recompiler/ShaderCacheVersion.cmake @@ -36,11 +36,10 @@ endforeach() set(failures) set(manifest "${EXTRA}\n") set(switches) -foreach(source IN LISTS sources) +foreach(source sourceReal IN ZIP_LISTS sources sourcesReal) if(NOT EXISTS "${source}") continue() endif() - get_filename_component(sourceReal "${source}" REALPATH) file(READ "${source}" text) string(REPLACE "\r\n" "\n" text "${text}") string(SHA256 digest "${text}") @@ -65,7 +64,11 @@ foreach(source IN LISTS sources) set(resolved) foreach(directory IN ITEMS "${sourceDirectory}" ${includeDirectories}) if(EXISTS "${directory}/${included}" AND NOT IS_DIRECTORY "${directory}/${included}") - get_filename_component(resolved "${directory}/${included}" REALPATH) + string(SHA1 includedKey "${directory}/${included}") + if(NOT DEFINED includedReal_${includedKey}) + get_filename_component(includedReal_${includedKey} "${directory}/${included}" REALPATH) + endif() + set(resolved "${includedReal_${includedKey}}") break() endif() endforeach() From 16eb32f20ef6f9b481a5056d783c67fc2f7c884e Mon Sep 17 00:00:00 2001 From: Bram910 Date: Sat, 24 Oct 2026 16:46:21 +0200 Subject: [PATCH 220/428] fix(relinker): resolve BND and NOTRACK call sites --- .../src/analysis/CallSiteResolver.cpp | 5 +++++ .../relinker/tests/CallSiteResolverTests.cpp | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp index b615250ae8..a99edf14ee 100644 --- a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp +++ b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp @@ -30,6 +30,11 @@ std::optional ripRelativeDispOffset(const std::vector ++cur; continue; } + if (b == PrefixRepne || b == PrefixSegDs) { + hasRexW = false; + ++cur; + continue; + } break; } if (cur + 2 > end) return std::nullopt; diff --git a/core/relinker/relinker/tests/CallSiteResolverTests.cpp b/core/relinker/relinker/tests/CallSiteResolverTests.cpp index 6dd410e448..06fed60f07 100644 --- a/core/relinker/relinker/tests/CallSiteResolverTests.cpp +++ b/core/relinker/relinker/tests/CallSiteResolverTests.cpp @@ -33,6 +33,28 @@ int main() { check("multiple REX prefixes on pointer load", {0x40, 0x48, 0x8b, 0x05, 0xf8, 0, 0, 0}, true); check("earlier REX.W does not widen a pointer load", {0x48, 0x40, 0x8b, 0x05, 0xf8, 0, 0, 0}, false); check("final REX.W widens a pointer load", {0x48, 0x40, 0x48, 0x8b, 0x05, 0xf7, 0, 0, 0}, true); + check("BND indirect call", {0xf2, 0xff, 0x15, 0xf9, 0, 0, 0}, true); + check("BND indirect tail jump", {0xf2, 0xff, 0x25, 0xf9, 0, 0, 0}, true); + check("NOTRACK indirect call", {0x3e, 0xff, 0x15, 0xf9, 0, 0, 0}, true); + check("NOTRACK indirect tail jump", {0x3e, 0xff, 0x25, 0xf9, 0, 0, 0}, true); + check("NOTRACK BND indirect call", {0x3e, 0xf2, 0xff, 0x15, 0xf8, 0, 0, 0}, true); + check("BND NOTRACK indirect tail jump", {0xf2, 0x3e, 0xff, 0x25, 0xf8, 0, 0, 0}, true); + check("BND REX indirect call", {0xf2, 0x48, 0xff, 0x15, 0xf8, 0, 0, 0}, true); + check("NOTRACK REX indirect tail jump", {0x3e, 0x48, 0xff, 0x25, 0xf8, 0, 0, 0}, true); + check("DS-prefixed pointer load", {0x3e, 0x48, 0x8b, 0x05, 0xf8, 0, 0, 0}, true); + check("REPNE-prefixed pointer load", {0xf2, 0x48, 0x8b, 0x05, 0xf8, 0, 0, 0}, true); + check("DS after REX.W leaves a narrow load", {0x48, 0x3e, 0x8b, 0x05, 0xf8, 0, 0, 0}, false); + check("REPNE after REX.W leaves a narrow load", {0x48, 0xf2, 0x8b, 0x05, 0xf8, 0, 0, 0}, false); + check("final REX.W after DS widens a load", {0x48, 0x3e, 0x48, 0x8b, 0x05, 0xf7, 0, 0, 0}, true); + check("final REX.W after REPNE widens a load", {0x48, 0xf2, 0x48, 0x8b, 0x05, 0xf7, 0, 0, 0}, true); + check("NOTRACK preserves final narrow REX", {0x3e, 0x48, 0x40, 0x8b, 0x05, 0xf7, 0, 0, 0}, false); + check("FS call is not a flat GOT reference", {0x64, 0xf2, 0xff, 0x15, 0xf8, 0, 0, 0}, false); + check("GS jump is not a flat GOT reference", {0x3e, 0x65, 0xff, 0x25, 0xf8, 0, 0, 0}, false); + check("address-size override is not a RIP64 reference", {0x3e, 0x67, 0xff, 0x15, 0xf8, 0, 0, 0}, false); + check("BND register call has no RIP operand", {0xf2, 0xff, 0xd0}, false); + check("NOTRACK register jump has no RIP operand", {0x3e, 0xff, 0xe0}, false); + check("BND-prefixed increment is not an import access", {0xf2, 0xff, 0x05, 0xf9, 0, 0, 0}, false); + check("DS-prefixed decrement is not an import access", {0x3e, 0xff, 0x0d, 0xf9, 0, 0, 0}, false); check("pointer load without REX.W", {0x40, 0x8b, 0x05, 0xf9, 0, 0, 0}, false); check("register pointer load", {0x48, 0x8b, 0x00}, false); check("increment memory", {0xff, 0x05, 0xfa, 0, 0, 0}, false); From 7f58c7a407aed7cb626f90f3212bc392f141f7d7 Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Sat, 24 Oct 2026 16:51:25 +0200 Subject: [PATCH 221/428] fix(relinker): reject R_X86_64_COPY relocations The Linux output writes every import symbol with st_size 0 and copies a COPY relocation into the output unchanged, so ld.so copies MIN(st_size of the definition, 0) = 0 bytes and the variable stays zero. Windows and macOS already rejected the type in the relocation builder. ValidationPolicy now rejects type 5 before any target code, so every target reports the same error, and the COPY case of the fixed-base rebasing, which validation makes unreachable, is gone. --- core/relinker/CMakeLists.txt | 2 + .../elfpatcher/src/linux/LinuxElfPatcher.cpp | 1 - .../src/analysis/ValidationPolicy.cpp | 1 - .../relinker/tests/test_copy_relocations.py | 62 +++++++++++++++++++ 4 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 core/relinker/relinker/tests/test_copy_relocations.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 97e979dd21..f5c786c2a4 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -764,6 +764,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_call_registry.py $) add_test(NAME jmprel_table_bounds COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_jmprel_table_bounds.py $) + add_test(NAME copy_relocations COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_copy_relocations.py $) if(WIN32) add_test(NAME windows_diagnostic_fixture_isolation COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/test_windows_diagnostic_fixture_isolation.py $) diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index b2f84ae52d..2c406e732b 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -81,7 +81,6 @@ std::vector RebasedRelocations(const std::vector& ta } case R_X86_64_64: case R_X86_64_PC32: - case R_X86_64_COPY: case R_X86_64_GLOB_DAT: case R_X86_64_JUMP_SLOT: case R_X86_64_32: diff --git a/core/relinker/relinker/src/analysis/ValidationPolicy.cpp b/core/relinker/relinker/src/analysis/ValidationPolicy.cpp index 47d65ac011..b9d67a3069 100644 --- a/core/relinker/relinker/src/analysis/ValidationPolicy.cpp +++ b/core/relinker/relinker/src/analysis/ValidationPolicy.cpp @@ -11,7 +11,6 @@ void ValidationPolicy::_initializeSupportedRelocationTypes() { R_X86_64_NONE, R_X86_64_64, R_X86_64_PC32, - R_X86_64_COPY, R_X86_64_GLOB_DAT, R_X86_64_JUMP_SLOT, R_X86_64_RELATIVE, diff --git a/core/relinker/relinker/tests/test_copy_relocations.py b/core/relinker/relinker/tests/test_copy_relocations.py new file mode 100644 index 0000000000..5a1677e26e --- /dev/null +++ b/core/relinker/relinker/tests/test_copy_relocations.py @@ -0,0 +1,62 @@ +"""R_X86_64_COPY relocations in DT_RELA and in the PLT table are rejected by validation, before any +target code runs, with the same error for every output target; GLOB_DAT and JUMP_SLOT are accepted. The +Linux output gives every import symbol st_size 0, so a dynamic loader would copy MIN(st_size of the +definition, 0) = 0 bytes and the variable would stay zero.""" + +from pathlib import Path +import subprocess +import sys +import tempfile + +from test_jmprel_table_bounds import fixture + +R_X86_64_COPY = 5 +TARGETS = {(): "linux", ("--lazy-binding",): "lazy", ("--windows",): "windows", ("--macos",): "macos"} +ERROR = "Unsupported relocation type 0x5" + +CASES = [ + ("copy-rela", fixture(linux=True, data_relocations=True, relocation_type=R_X86_64_COPY), True), + ("copy-jmprel", fixture(linux=True, relocation_type=R_X86_64_COPY), True), + ("glob-dat-rela", fixture(linux=True, data_relocations=True), False), + ("jump-slot-jmprel", fixture(linux=True), False), +] + + +def relink(relinker, work, name, image, flags): + source = work / (name + ".elf") + output = work / (name + ".out") + source.write_bytes(image) + result = subprocess.run([str(relinker), "--skip-sce-module", *flags, str(source), str(output)], + capture_output=True, text=True, timeout=20) + return result, output + + +def fail_lines(result): + return [line for line in result.stderr.splitlines() if line.startswith("FAIL: ")] + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix="anyps5-copy-") as directory: + work = Path(directory) + for case, image, rejected in CASES: + failures = {} + for flags, target in TARGETS.items(): + name = f"{case}-{target}" + result, output = relink(relinker, work, name, image, flags) + if rejected: + assert result.returncode == 2 and ERROR in result.stderr and not output.exists(), ( + name, result.returncode, result.stdout, result.stderr) + failures[target] = fail_lines(result) + else: + assert result.returncode == 0 and output.exists(), ( + name, result.returncode, result.stdout, result.stderr) + output.unlink() + if rejected: + assert failures["linux"] and all(lines == failures["linux"] for lines in failures.values()), ( + case, failures) + print("COPY relocation test passed") + + +if __name__ == "__main__": + main() From 753767f86a872f8656028e6475c69e4ce1d72503 Mon Sep 17 00:00:00 2001 From: TechLevelZero <35297505+TechLevelZero@users.noreply.github.com> Date: Sat, 24 Oct 2026 12:11:57 +0100 Subject: [PATCH 222/428] fix(shader): translate f32 conversions in f32 denormal modes 1 and 2 The f32 conversions that only flush their inputs follow bit 0 of the FLOAT_MODE f32 denormal field, as the f32 compares do: mode 1 keeps denormal inputs like mode 3 and mode 2 flushes them like mode 0. v_cvt_pk_u8_f32 also follows a round mode set at run time. Measured on RDNA2 (gfx1036) with the hardware oracle. Co-Authored-By: Claude Opus 5.5 --- .../execution/F32ConvertDenormalModes.cpp | 193 ++++++++++++++++++ .../tests/execution/VCvtPkU8F32RoundMode.cpp | 23 ++- .../Translation/src/RoundingMode.cpp | 2 +- .../Translation/src/VectorInstructions.cpp | 2 +- docs/dev/debt/shader.md | 7 +- 5 files changed, 215 insertions(+), 12 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/F32ConvertDenormalModes.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F32ConvertDenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32ConvertDenormalModes.cpp new file mode 100644 index 0000000000..5720a9ddae --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32ConvertDenormalModes.cpp @@ -0,0 +1,193 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 16; +constexpr std::uint32_t Columns = 12; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0x34040086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e141104u, 0x7e160f04u, 0x7e181904u, + 0x7e1a1b04u, 0x7e1c7f04u, 0x7e1e2104u, 0xd7680011u, 0x00020b04u, 0xd7690012u, 0x00020b04u, 0xd55e0013u, + 0x02010104u, 0x5e280b04u, 0x7e2a1504u, 0xe0781000u, 0x80010a02u, 0xe0781010u, 0x80010e02u, 0xe0781020u, + 0x80011202u, 0xbf810000u, +}; + +constexpr const char* Names[Columns] = { + "v_cvt_i32_f32 v10, v4", + "v_cvt_u32_f32 v11, v4", + "v_cvt_rpi_i32_f32 v12, v4", + "v_cvt_flr_i32_f32 v13, v4", + "v_frexp_exp_i32_f32 v14, v4", + "v_cvt_f64_f32 v[15:16], v4 (low)", + "v_cvt_f64_f32 v[15:16], v4 (high)", + "v_cvt_pknorm_i16_f32 v17, v4, v5", + "v_cvt_pknorm_u16_f32 v18, v4, v5", + "v_cvt_pk_u8_f32 v19, v4, 0, 0", + "v_cvt_pkrtz_f16_f32 v20, v4, v5", + "v_cvt_f16_f32 v21, v4", +}; + +struct ConvertVector { + std::uint32_t operands[2]; + std::uint32_t denormalsFlushed[Columns]; + std::uint32_t denormalsKept[Columns]; +}; + +constexpr std::array Vectors{{ + {{0x00000001u, 0x00400000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffff6cu, 0x00000000u, 0x36a00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {{0x007fffffu, 0x80000001u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffff82u, 0xc0000000u, 0x380fffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u}}, + {{0x00400000u, 0x3f800000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x7fff0000u, 0xffff0000u, 0x00000000u, 0x3c000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffff82u, 0x00000000u, 0x38000000u, 0x7fff0000u, 0xffff0000u, 0x00000000u, 0x3c000000u, 0x00000000u}}, + {{0x80000001u, 0x007fffffu}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0xffffffffu, 0xffffff6cu, 0x00000000u, 0xb6a00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u}}, + {{0x807fffffu, 0x00000001u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0xffffffffu, 0xffffff82u, 0xc0000000u, 0xb80fffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u}}, + {{0x80400000u, 0xbf000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0xc0000000u, 0x00000000u, 0x00000000u, 0xb8008000u, 0x00008000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0xffffffffu, 0xffffff82u, 0x00000000u, 0xb8000000u, 0xc0000000u, 0x00000000u, 0x00000000u, 0xb8008000u, 0x00008000u}}, + {{0x00800000u, 0x80400000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffff83u, 0x00000000u, 0x38100000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffff83u, 0x00000000u, 0x38100000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u}}, + {{0x80800000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0xffffffffu, 0xffffff83u, 0x00000000u, 0xb8100000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0xffffffffu, 0xffffff83u, 0x00000000u, 0xb8100000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u}}, + {{0x3f000000u, 0x00000001u}, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3fe00000u, 0x00004000u, 0x00008000u, 0x00000000u, 0x00003800u, 0x00003800u}, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3fe00000u, 0x00004000u, 0x00008000u, 0x00000000u, 0x00003800u, 0x00003800u}}, + {{0xbfc00000u, 0x807fffffu}, {0xffffffffu, 0x00000000u, 0xffffffffu, 0xfffffffeu, 0x00000001u, 0x00000000u, 0xbff80000u, 0x00008001u, 0x00000000u, 0x00000000u, 0x8000be00u, 0x0000be00u}, {0xffffffffu, 0x00000000u, 0xffffffffu, 0xfffffffeu, 0x00000001u, 0x00000000u, 0xbff80000u, 0x00008001u, 0x00000000u, 0x00000000u, 0x8000be00u, 0x0000be00u}}, + {{0x3f800000u, 0x80000002u}, {0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000000u, 0x3ff00000u, 0x00007fffu, 0x0000ffffu, 0x00000001u, 0x80003c00u, 0x00003c00u}, {0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000000u, 0x3ff00000u, 0x00007fffu, 0x0000ffffu, 0x00000001u, 0x80003c00u, 0x00003c00u}}, + {{0x00000000u, 0x00000002u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {{0x80000000u, 0x3f800000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x7fff0000u, 0xffff0000u, 0x00000000u, 0x3c008000u, 0x00008000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x7fff0000u, 0xffff0000u, 0x00000000u, 0x3c008000u, 0x00008000u}}, + {{0x33800000u, 0x00000001u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffffe9u, 0x00000000u, 0x3e700000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffffe9u, 0x00000000u, 0x3e700000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u}}, + {{0x477fe000u, 0x80000001u}, {0x0000ffe0u, 0x0000ffe0u, 0x0000ffe0u, 0x0000ffe0u, 0x00000010u, 0x00000000u, 0x40effc00u, 0x00007fffu, 0x0000ffffu, 0x000000ffu, 0x80007bffu, 0x00007bffu}, {0x0000ffe0u, 0x0000ffe0u, 0x0000ffe0u, 0x0000ffe0u, 0x00000010u, 0x00000000u, 0x40effc00u, 0x00007fffu, 0x0000ffffu, 0x000000ffu, 0x80007bffu, 0x00007bffu}}, + {{0x501502f9u, 0x00800000u}, {0x7fffffffu, 0xffffffffu, 0x7fffffffu, 0x7fffffffu, 0x00000022u, 0x20000000u, 0x4202a05fu, 0x00007fffu, 0x0000ffffu, 0x000000ffu, 0x00007bffu, 0x00007c00u}, {0x7fffffffu, 0xffffffffu, 0x7fffffffu, 0x7fffffffu, 0x00000022u, 0x20000000u, 0x4202a05fu, 0x00007fffu, 0x0000ffffu, 0x000000ffu, 0x00007bffu, 0x00007c00u}}, + {{0xd01502f9u, 0x007fffffu}, {0x80000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000022u, 0x20000000u, 0xc202a05fu, 0x00008001u, 0x00000000u, 0x00000000u, 0x0000fbffu, 0x0000fc00u}, {0x80000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000022u, 0x20000000u, 0xc202a05fu, 0x00008001u, 0x00000000u, 0x00000000u, 0x0000fbffu, 0x0000fc00u}}, + {{0x7f800000u, 0xbfc00000u}, {0x7fffffffu, 0xffffffffu, 0x7fffffffu, 0x7fffffffu, 0x00000000u, 0x00000000u, 0x7ff00000u, 0x80017fffu, 0x0000ffffu, 0x000000ffu, 0xbe007c00u, 0x00007c00u}, {0x7fffffffu, 0xffffffffu, 0x7fffffffu, 0x7fffffffu, 0x00000000u, 0x00000000u, 0x7ff00000u, 0x80017fffu, 0x0000ffffu, 0x000000ffu, 0xbe007c00u, 0x00007c00u}}, + {{0xff800000u, 0x00000001u}, {0x80000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0xfff00000u, 0x00008001u, 0x00000000u, 0x00000000u, 0x0000fc00u, 0x0000fc00u}, {0x80000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0xfff00000u, 0x00008001u, 0x00000000u, 0x00000000u, 0x0000fc00u, 0x0000fc00u}}, + {{0x7fc00000u, 0x80400000u}, {0x00000000u, 0x00000000u, 0x7fffffffu, 0x7fffffffu, 0x00000000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80007e00u, 0x00007e00u}, {0x00000000u, 0x00000000u, 0x7fffffffu, 0x7fffffffu, 0x00000000u, 0x00000000u, 0x7ff80000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80007e00u, 0x00007e00u}}, + {{0xffc00000u, 0x00400000u}, {0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0xfff80000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000fe00u, 0x0000fe00u}, {0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0xfff80000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000fe00u, 0x0000fe00u}}, + {{0x4f800000u, 0x80000000u}, {0x7fffffffu, 0xffffffffu, 0x7fffffffu, 0x7fffffffu, 0x00000021u, 0x00000000u, 0x41f00000u, 0x00007fffu, 0x0000ffffu, 0x000000ffu, 0x80007bffu, 0x00007c00u}, {0x7fffffffu, 0xffffffffu, 0x7fffffffu, 0x7fffffffu, 0x00000021u, 0x00000000u, 0x41f00000u, 0x00007fffu, 0x0000ffffu, 0x000000ffu, 0x80007bffu, 0x00007c00u}}, + {{0xcf000000u, 0x007fffffu}, {0x80000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000020u, 0x00000000u, 0xc1e00000u, 0x00008001u, 0x00000000u, 0x00000000u, 0x0000fbffu, 0x0000fc00u}, {0x80000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000020u, 0x00000000u, 0xc1e00000u, 0x00008001u, 0x00000000u, 0x00000000u, 0x0000fbffu, 0x0000fc00u}}, + {{0x3f7fffffu, 0x80000001u}, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0xe0000000u, 0x3fefffffu, 0x00007fffu, 0x0000ffffu, 0x00000001u, 0x80003bffu, 0x00003c00u}, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0xe0000000u, 0x3fefffffu, 0x00007fffu, 0x0000ffffu, 0x00000001u, 0x80003bffu, 0x00003c00u}}, + {{0x00000002u, 0x3f800000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x7fff0000u, 0xffff0000u, 0x00000000u, 0x3c000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffff6du, 0x00000000u, 0x36b00000u, 0x7fff0000u, 0xffff0000u, 0x00000000u, 0x3c000000u, 0x00000000u}}, + {{0x80000002u, 0x00000003u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0xffffffffu, 0xffffff6du, 0x00000000u, 0xb6b00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u}}, + {{0x00000001u, 0x00000001u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffff6cu, 0x00000000u, 0x36a00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {{0x007fffffu, 0x807fffffu}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffff82u, 0xc0000000u, 0x380fffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u}}, + {{0x3f000000u, 0x80000001u}, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3fe00000u, 0x00004000u, 0x00008000u, 0x00000000u, 0x80003800u, 0x00003800u}, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3fe00000u, 0x00004000u, 0x00008000u, 0x00000000u, 0x80003800u, 0x00003800u}}, + {{0x80000001u, 0x00400000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0xffffffffu, 0xffffff6cu, 0x00000000u, 0xb6a00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u}}, + {{0x3e800000u, 0x80800000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffffffu, 0x00000000u, 0x3fd00000u, 0x00002000u, 0x00004000u, 0x00000000u, 0x80003400u, 0x00003400u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffffffu, 0x00000000u, 0x3fd00000u, 0x00002000u, 0x00004000u, 0x00000000u, 0x80003400u, 0x00003400u}}, + {{0xbf800000u, 0x00000001u}, {0xffffffffu, 0x00000000u, 0xffffffffu, 0xffffffffu, 0x00000001u, 0x00000000u, 0xbff00000u, 0x00008001u, 0x00000000u, 0x00000000u, 0x0000bc00u, 0x0000bc00u}, {0xffffffffu, 0x00000000u, 0xffffffffu, 0xffffffffu, 0x00000001u, 0x00000000u, 0xbff00000u, 0x00008001u, 0x00000000u, 0x00000000u, 0x0000bc00u, 0x0000bc00u}}, +}}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::vector Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode floatMode) { + Input.fill(0u); + for (std::uint32_t lane = 0; lane < Vectors.size(); ++lane) { + Input[lane * Inputs] = Vectors[lane].operands[0]; + Input[lane * Inputs + 1u] = Vectors[lane].operands[1]; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = floatMode; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + return {Output.begin(), Output.end()}; +} + +std::string Where(std::uint32_t lane, std::uint32_t column, std::uint32_t denormals, bool ieee) { + return std::string(Names[column]) + " of " + Hex(Vectors[lane].operands[0]) + ", " + Hex(Vectors[lane].operands[1]) + " in f32 denormal mode " + + std::to_string(denormals) + (ieee ? ", IEEE=1" : ", IEEE=0"); +} + +void CheckFlushed(const std::vector& output, std::uint32_t denormals, bool ieee) { + for (std::uint32_t lane = 0; lane < Vectors.size(); ++lane) { + for (std::uint32_t column = 0; column < Columns; ++column) { + const auto value = output[lane * Results + column]; + const auto expected = Vectors[lane].denormalsFlushed[column]; + Require(value == expected, Where(lane, column, denormals, ieee) + " is " + Hex(value) + ", expected " + Hex(expected)); + } + } +} + +void CheckKept(const std::vector& output, bool ieee) { + for (std::uint32_t lane = 0; lane < Vectors.size(); ++lane) { + for (std::uint32_t column = 0; column < Columns; ++column) { + const auto value = output[lane * Results + column]; + const auto& vector = Vectors[lane]; + Require(value == vector.denormalsKept[column] || value == vector.denormalsFlushed[column], + Where(lane, column, 3u, ieee) + " is " + Hex(value) + ", expected " + Hex(vector.denormalsKept[column]) + " or, on a driver that flushes f32 denormals, " + + Hex(vector.denormalsFlushed[column])); + } + } +} + +void CheckSame(const std::vector& output, const std::vector& reference, std::uint32_t denormals, std::uint32_t like, bool ieee) { + for (std::uint32_t lane = 0; lane < Vectors.size(); ++lane) { + for (std::uint32_t column = 0; column < Columns; ++column) { + const auto value = output[lane * Results + column]; + const auto expected = reference[lane * Results + column]; + Require(value == expected, Where(lane, column, denormals, ieee) + " is " + Hex(value) + ", but mode " + std::to_string(like) + " gave " + Hex(expected)); + } + } +} + +ShaderRecompiler::ShaderFloatMode Mode(std::uint32_t denormals, bool ieee) { + return ShaderRecompiler::ShaderFloatMode{0xc0u | denormals << 4u, true, ieee, false}; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const bool ieee : {false, true}) { + const auto flushed = Run(*device, Mode(0u, ieee)); + CheckFlushed(flushed, 0u, ieee); + const auto inputsFlushed = Run(*device, Mode(2u, ieee)); + CheckFlushed(inputsFlushed, 2u, ieee); + CheckSame(inputsFlushed, flushed, 2u, 0u, ieee); + const auto kept = Run(*device, Mode(3u, ieee)); + CheckKept(kept, ieee); + const auto outputsFlushed = Run(*device, Mode(1u, ieee)); + CheckSame(outputsFlushed, kept, 1u, 3u, ieee); + } + std::puts("f32 conversion denormal mode tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VCvtPkU8F32RoundMode.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtPkU8F32RoundMode.cpp index 777bd3e290..88725601dd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VCvtPkU8F32RoundMode.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtPkU8F32RoundMode.cpp @@ -26,6 +26,11 @@ alignas(256) constexpr std::array Code{ 0x80010a02u, 0xbf810000u, }; +alignas(256) std::array RoundModeCode{ + 0x34020084u, 0x34040082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xbfa40000u, 0xd55e000au, 0x02010104u, + 0xe0701000u, 0x80010a02u, 0xbf810000u, +}; + struct PackVector { std::uint32_t value; std::uint32_t denormalsFlushed[4]; @@ -84,7 +89,7 @@ std::string Hex(std::uint32_t value) { return text; } -void Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode floatMode) { +void Run(AgcDriver::VulkanDevice& device, std::span code, ShaderRecompiler::ShaderFloatMode floatMode) { Input.fill(0u); for (std::uint32_t lane = 0; lane < Vectors.size(); ++lane) Input[lane * Inputs] = Vectors[lane].value; Output.fill(0xdeadbeefu); @@ -93,7 +98,6 @@ void Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode floa const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); std::copy(input.begin(), input.end(), userData.begin()); std::copy(output.begin(), output.end(), userData.begin() + 4); - const std::span code(Code); const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ @@ -109,11 +113,11 @@ void Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode floa device.WaitIdle(); } -void Check(bool kept, std::uint32_t rounding, bool ieee) { +void Check(bool kept, std::uint32_t rounding, bool ieee, const std::string& setting) { for (std::uint32_t lane = 0; lane < Vectors.size(); ++lane) { const auto& vector = Vectors[lane]; const auto expected = kept ? vector.denormalsKept[rounding] : vector.denormalsFlushed[rounding]; - Require(Output[lane] == expected, std::string("v_cvt_pk_u8_f32 round mode ") + std::to_string(rounding) + (kept ? ", f32 denormals kept" : ", f32 denormals flushed") + + Require(Output[lane] == expected, std::string("v_cvt_pk_u8_f32 round mode ") + std::to_string(rounding) + setting + (kept ? ", f32 denormals kept" : ", f32 denormals flushed") + (ieee ? ", IEEE=1" : ", IEEE=0") + " of " + Hex(vector.value) + " is " + Hex(Output[lane]) + ", expected " + Hex(expected)); } } @@ -124,11 +128,16 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - for (const bool kept : {false, true}) { + for (std::uint32_t denormals = 0; denormals < 4u; ++denormals) { + const bool kept = (denormals & 1u) != 0u; + const std::string field = ", f32 denormal mode " + std::to_string(denormals); for (std::uint32_t rounding = 0; rounding < 4u; ++rounding) { for (const bool ieee : {false, true}) { - Run(*device, ShaderRecompiler::ShaderFloatMode{(kept ? 0xf0u : 0xc0u) | rounding, true, ieee, false}); - Check(kept, rounding, ieee); + Run(*device, Code, ShaderRecompiler::ShaderFloatMode{0xc0u | denormals << 4u | rounding, true, ieee, false}); + Check(kept, rounding, ieee, field); + RoundModeCode[5] = 0xbfa40000u | rounding; + Run(*device, RoundModeCode, ShaderRecompiler::ShaderFloatMode{0xc0u | denormals << 4u, true, ieee, false}); + Check(kept, rounding, ieee, field + " set by s_round_mode"); } } } diff --git a/core/shader/recompiler/Translation/src/RoundingMode.cpp b/core/shader/recompiler/Translation/src/RoundingMode.cpp index 0edeca52f0..0e648acaa9 100644 --- a/core/shader/recompiler/Translation/src/RoundingMode.cpp +++ b/core/shader/recompiler/Translation/src/RoundingMode.cpp @@ -89,6 +89,7 @@ RoundingUse RoundingUseOf(RdnaOpcode opcode) { case RdnaOpcode::VCvtF32I32: case RdnaOpcode::VCvtF32U32: case RdnaOpcode::VLdexpF32: + case RdnaOpcode::VCvtPkU8F32: return {round32, RoundingUseKind::Directed32}; case RdnaOpcode::VCvtF16F32: case RdnaOpcode::VCvtF16U16: @@ -120,7 +121,6 @@ RoundingUse RoundingUseOf(RdnaOpcode opcode) { case RdnaOpcode::VMadmkF32: case RdnaOpcode::VMadakF32: case RdnaOpcode::VFractF32: - case RdnaOpcode::VCvtPkU8F32: case RdnaOpcode::VCvtF32F64: case RdnaOpcode::VDivScaleF32: case RdnaOpcode::VDivFmasF32: diff --git a/core/shader/recompiler/Translation/src/VectorInstructions.cpp b/core/shader/recompiler/Translation/src/VectorInstructions.cpp index c02cdba51e..4c9ec16add 100644 --- a/core/shader/recompiler/Translation/src/VectorInstructions.cpp +++ b/core/shader/recompiler/Translation/src/VectorInstructions.cpp @@ -93,7 +93,6 @@ std::uint32_t TranslationContext::f32DenormalFlushFor(const RdnaInstruction& ins case RdnaOpcode::VCmpxNleF32: case RdnaOpcode::VCmpxNlgF32: case RdnaOpcode::VCmpxNltF32: - return floatMode.has_value() && ((floatMode->floatMode >> 4u) & 1u) != 0u ? 0u : 1u; case RdnaOpcode::VCvtI32F32: case RdnaOpcode::VCvtU32F32: case RdnaOpcode::VCvtRpiI32F32: @@ -105,6 +104,7 @@ std::uint32_t TranslationContext::f32DenormalFlushFor(const RdnaInstruction& ins case RdnaOpcode::VCvtPkU8F32: case RdnaOpcode::VCvtPkrtzF16F32: case RdnaOpcode::VCvtF16F32: + return floatMode.has_value() && ((floatMode->floatMode >> 4u) & 1u) != 0u ? 0u : 1u; case RdnaOpcode::VDivScaleF32: case RdnaOpcode::VDivFixupF32: case RdnaOpcode::VMadMixloF16: diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 134b3b4632..5fa0c9107d 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -86,9 +86,9 @@ - [8_SRGB and 8_8_SRGB loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) on a device that cannot sample R8_SRGB or R8G8_SRGB return the sRGB EOTF rounded to nearest even at bf16 precision, as adriadam10 measured on RDNA2 (gfx1035) in #866; the PS5's gfx1013 is not measured. This is the path on a device that cannot blit R8 or RG8 to RGBA8, and with `APS5_SRGB_SHADER_DECODE=1`; otherwise a load of a sampled texture reads the RGBA8 sRGB copy and gets the device's own decode. - [Samples and gathers](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8_SRGB and 8_8_SRGB textures read an RGBA8 sRGB copy of the texture (libSceAgcDriver, [Texture.cpp](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp)) on a device that cannot sample R8_SRGB or R8G8_SRGB but can blit R8 or RG8 to RGBA8, so they use that device's own decode. They still throw on a device that cannot blit, and with `APS5_SRGB_SHADER_DECODE=1`. `APS5_SRGB_SHADER_DECODE=copy` also takes the copy on a device that samples these formats natively, so the copy can be tested on any device that can blit R8 or RG8 to RGBA8; on a device that cannot blit, it behaves like `APS5_SRGB_SHADER_DECODE=1`. A multisampled or depth view of these formats gets no copy. - [Bindless image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) entries of 8_SRGB and 8_8_SRGB textures read zeros, like converted formats, on a device that cannot sample R8_SRGB or R8G8_SRGB. -- [Shader MODE register access](../../../core/shader/recompiler/Translation/src/RoundingMode.cpp) starts from the rounding fields of `GuestContext::floatMode` and follows immediate rounding writes (`s_round_mode`, `s_setreg_imm32_b32` on MODE bits 0-3) through the control flow graph; `s_getreg_b32` of those bits returns the tracked value. Only `v_cvt_f32_i32`, `v_cvt_f32_u32`, `v_ldexp_f32`, the f16 conversions and `v_ldexp_f16` (with f16 denormals kept) and the instructions that already read `FLOAT_MODE` follow a rounding mode set at run time; any other float instruction that runs while a directed rounding mode differs from the initial one throws at that instruction. Rounding bits written from an SGPR (`s_setreg_b32`) or that differ between the paths reaching an instruction throw where they are read or used. A shader without `FLOAT_MODE` metadata throws on any rounding change. Non-rounding register reads still throw; `s_setreg_b32` writes to other hardware registers are still ignored. Tests follow the public RDNA2/LLVM contract, not console measurements. +- [Shader MODE register access](../../../core/shader/recompiler/Translation/src/RoundingMode.cpp) starts from the rounding fields of `GuestContext::floatMode` and follows immediate rounding writes (`s_round_mode`, `s_setreg_imm32_b32` on MODE bits 0-3) through the control flow graph; `s_getreg_b32` of those bits returns the tracked value. Only `v_cvt_f32_i32`, `v_cvt_f32_u32`, `v_ldexp_f32`, `v_cvt_pk_u8_f32`, the f16 conversions and `v_ldexp_f16` (with f16 denormals kept) and the instructions that already read `FLOAT_MODE` follow a rounding mode set at run time; any other float instruction that runs while a directed rounding mode differs from the initial one throws at that instruction. Rounding bits written from an SGPR (`s_setreg_b32`) or that differ between the paths reaching an instruction throw where they are read or used. A shader without `FLOAT_MODE` metadata throws on any rounding change. Non-rounding register reads still throw; `s_setreg_b32` writes to other hardware registers are still ignored. Tests follow the public RDNA2/LLVM contract, not console measurements. - [Flat scalar-read reachability](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) keeps both successors active for non-clean predicates and loop headers, so an inaccessible direct root on such a path still refuses capture. Skipping a read under a clean, evaluable scalar branch follows RDNA2 conditional PC semantics and synthetic fixtures; console execution and title behavior are unverified. -- [v_cvt_pk_u8_f32](../../../core/shader/recompiler/Translation/src/ConvertInstructions.cpp) throws in f32 denormal modes 1 and 2 and with a directed round mode set at run time (see the previous entry); the PS5's gfx1013 is not measured. Otherwise it rounds its source in the `FLOAT_MODE` f32 round mode (ties to even, toward +infinity, -infinity or zero), clamps it to 0..255 (a NaN gives 0) and flushes a denormal source when f32 denormals are flushed. A kept positive denormal gives 1 toward +infinity, decided on the bits, as a driver without `DenormPreserve 32` may flush it. Measured on RDNA2 (gfx1030, RX 6800, AMD Windows driver 32.0.21045.5002) over 2,133 sources (k/8 for k = -16 to 2,099, values next to 0.5 and 255.5, ±1e10, denormals, infinities, NaNs) at IEEE 0 and 1, f32 denormal modes 0 and 3 and f32 round modes 0 to 3, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even, FP16_OVFL=0. +- [v_cvt_pk_u8_f32](../../../core/shader/recompiler/Translation/src/ConvertInstructions.cpp): the PS5's gfx1013 is not measured. It rounds its source in the `FLOAT_MODE` f32 round mode (ties to even, toward +infinity, -infinity or zero), clamps it to 0..255 (a NaN gives 0) and flushes a denormal source when f32 denormals are flushed. A kept positive denormal gives 1 toward +infinity, decided on the bits, as a driver without `DenormPreserve 32` may flush it. Measured on RDNA2 (gfx1030, RX 6800, AMD Windows driver 32.0.21045.5002) over 2,133 sources (k/8 for k = -16 to 2,099, values next to 0.5 and 255.5, ±1e10, denormals, infinities, NaNs) at IEEE 0 and 1, f32 denormal modes 0 and 3 and f32 round modes 0 to 3, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even, FP16_OVFL=0. A round mode set by `s_round_mode` gives the results of the same mode in `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2,148 such sources in every f32 denormal and round mode. - A [flat SRT read](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) through a null or unmapped pointer loaded from memory raises a [BDA fault](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp) only if the program executes it, then stops or reads zero like a BDA read. - Repeated `s_load`s of one [flat SRT](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) dword through a null or unmapped pointer report one of their pcs, and an `s_load` whose inaccessible dwords are all unused, or only address repeated loads, reports no fault of its own. - A value read through a null or unmapped pointer still throws at record time if a [uniform fill or table base](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) needs it, or if the pointer comes from user data and is not null, even if the console skips the read. @@ -126,7 +126,7 @@ - [f16 unary, frexp and conversion denormals](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field: `v_ceil_f16`, `v_floor_f16`, `v_fract_f16`, `v_rcp_f16`, `v_rsq_f16`, `v_sqrt_f16`, `v_log_f16`, `v_exp_f16`, `v_sin_f16`, `v_frexp_mant_f16`, `v_frexp_exp_i16_f16`, `v_cvt_norm_i16_f16`, `v_cvt_norm_u16_f16` and `v_cvt_f32_f16` flush a denormal half input to its signed zero when bit 0 is clear (0 and 2), and the f16-result ones flush a denormal half result to its signed zero when bit 1 is clear (0 and 1), after the host rounds it to half. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with denorm16 0..3 over every non-NaN f16 input (63,490 rows per mode and instruction): the only mismatches are the 1-ULP driver differences of `v_exp_f16` (2 inputs) and `v_sin_f16` (14), which are the same with denormals kept. NaN inputs are not compared. `v_ldexp_f16` shares the input rule. `v_cvt_pknorm_i16_f16` and `v_cvt_pknorm_u16_f16` flush both half inputs by the same bit, measured on RDNA2 (gfx1030) over every f16 input (each half also negated in the other source) at IEEE=0 and IEEE=1 with denorm16 0..3 and at f16 rounding 1..3: every row matches. Shaders without a known float mode keep the previous lowering. - [v_div_fixup_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) flushes its src1 and src2 to a zero of the same sign when bit 0 of the `FLOAT_MODE` f16 denormal field is clear (0 and 2) and keeps a denormal src0 and result in every mode, as `v_div_fixup_f32` does with the f32 field. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) over 6859 triples of 19 halves (zeros, denormals, minimum normals, ±0.5, ±1, the largest finite value, infinities and quiet and signaling NaNs) at IEEE=0 and IEEE=1 with denorm16 0..3 and FP16_OVFL 0 and 1, also with neg and abs source modifiers and with op_sel high halves. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [Packed f16 NaN results](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding throws. In the f16 denormal modes that flush, each denormal input lane is flushed to a zero of its sign when bit 0 of the field is clear (0 and 2), so `inf * 0x0001` is an invalid product there, and when bit 1 is clear (0 and 1) a `v_pk_add_f16` result below 2^-14, or a `v_pk_mul_f16`, `v_pk_fma_f16` or `v_pk_fmac_f16` result whose exact value rounded to 11 significant bits with an unbounded exponent is below 2^-14, is flushed to a zero of its sign; `v_pk_min/max_f16` flush only their inputs. The native f16x2 add and multiply is not used while results are flushed. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3, over 19,192 to 162,204 lanes per instruction with op_sel, op_sel_hi, neg_lo, neg_hi and clamp variants and products and sums at the 2^-14 boundary. -- [f32 denormals](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; Its f16 sources are flushed to a zero of their sign when bit 0 of the f16/f64 denormal field is clear (0 and 2), whatever the f32 field, measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with f32 denormal modes 0 and 3 and f16 denormal modes 0 to 3, over 2744 triples of 14 halves with all three sources f16 and 1960 rows with an f32 multiplicand. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions except the f32 compares. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` and the image, buffer, global and flat `fmin/fmax/fcmpswap` flush both operands by the field, see the float atomics entries. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` and the f32 compares are the exceptions. `v_cmp_X_f32` and `v_cmpx_X_f32` compare the operands as sign-magnitude integers with the NaN cases handled apart, so a denormal is not read as zero, measured on RDNA2 (gfx1036) over 256 operand pairs per opcode at IEEE=0 and IEEE=1 with f32 denormals kept and flushed. `v_ldexp_f32` preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. +- [f32 denormals](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; Its f16 sources are flushed to a zero of their sign when bit 0 of the f16/f64 denormal field is clear (0 and 2), whatever the f32 field, measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with f32 denormal modes 0 and 3 and f16 denormal modes 0 to 3, over 2744 triples of 14 halves with all three sources f16 and 1960 rows with an f32 multiplicand. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions except the f32 compares and the f32 conversions in their entries. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` and the image, buffer, global and flat `fmin/fmax/fcmpswap` flush both operands by the field, see the float atomics entries. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` and the f32 compares are the exceptions. `v_cmp_X_f32` and `v_cmpx_X_f32` compare the operands as sign-magnitude integers with the NaN cases handled apart, so a denormal is not read as zero, measured on RDNA2 (gfx1036) over 256 operand pairs per opcode at IEEE=0 and IEEE=1 with f32 denormals kept and flushed. `v_ldexp_f32` preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. Outside those bands, a driver result more than 4 ULP from a Taylor series in turns of the exact cycle is replaced by the series, which is within 2 ULP of the true sine and cosine; the hardware is up to 9 ULP from them (gfx1035, 243,092 f32 inputs), so results stay within 13 ULP of the hardware on any driver. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's own sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP; the series replaces those results. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. - [v_rcp_f32 and v_rcp_iflag_f32 with `APS5_EXACT_RCP`](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterReciprocal.cpp) return the hardware reciprocal for normal inputs with normal results: a cubic over 32 mantissa segments, the linear term rounded to nearest even, plus 2,761 one-ULP corrections. It matches all 2^23 mantissas of an RDNA2 (gfx1031) sweep; inputs or results that are not normal keep the driver's 1/x. It is off by default because the coefficient and correction tables add about 13k SPIR-V words to each shader that uses it. The switch also reaches v_rcp_f16 (rounded from the f32 reciprocal) and the pixel shader's position W input. - [v_ldexp_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f32 denormal field: mode 1 keeps a subnormal input and flushes a result below 2^-126 to a zero of its sign in every f32 round mode, also one that rounds to 2^-126 at subnormal precision (`0x3fffffff` scaled by 2^-127); mode 2 flushes a subnormal input to a zero of its sign and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, as mode 3 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 10,260 rows (54 inputs of both signs: zeros, subnormals, normals near 2^-126 and 1.0, the largest finite value, infinities and NaNs, each scaled by 190 exponents from -2^31 to 2^31-1) at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3 and f32 round mode 0 to 3, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0; with the `clamp` modifier the result is clamped as in the other modes. The PS5's gfx1013 is not measured. @@ -135,6 +135,7 @@ - [Ordered not-equal, less-than and greater-than compares](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual`, and `FPOrdLessThan32` and `FPOrdGreaterThan32` as their ordered forms, each with both operands checked for NaN on their bits; the results are the same for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of not-equal feeds a ballot on a 32-wide subgroup (#825), and so does a less-than or greater-than against an infinity the driver sees as a constant (#3536), presumably rewritten into a not-equal by its compiler - [16-bit integer multiplies](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`v_mul_lo_u16`, `v_pk_mul_lo_u16`) (shader recompiler) - a factor taken from the low half of its source is read as the low 24 bits of the register, not the low 16; the low 16 bits of the product are the same for every input. The AMD Windows driver 32.0.21045.5002 (RX 6700 XT, RDNA 2) stores the full 32-bit product when two values it knows fit in 16 bits are multiplied, the product is reduced to 16 bits and nothing else is merged into the stored value: `v_mul_lo_u16` into a register whose high half is zero, also with factors masked to 16 bits by the program, and `v_pk_mul_lo_u16` whose high lanes are zero wrote 0xfffe0001 for 0xffff * 0xffff where the GPU writes 0x00000001. Reducing the product with `v_and_b32`, `v_bfe_u32` or a shift pair loses the same bits; factors read as 24-bit fields do not. The [constant folder](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) keeps a field read that a multiply uses, even when the program already masked the factor to 16 bits, since folding it away brings the full product back (#8271). Measured against the same instructions run through the [hardware oracle](../HW_ORACLE.md) on that GPU; other drivers are not checked - [f32 compares in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) (`v_cmp_X_f32` and `v_cmpx_X_f32` with X = eq, ge, gt, le, lg, lt, neq, nge, ngt, nle, nlg, nlt) flush their f32 denormal inputs by bit 0 of the `FLOAT_MODE` f32 denormal field: mode 1 keeps them as mode 3 does, and mode 2 flushes them as mode 0 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 484 operand pairs of zeros, denormals, minimum normals, ones, the largest finite value, infinities and quiet and signaling NaNs of both signs, in VOPC form, at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3, f32 round to nearest even, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0. The PS5's gfx1013 is not measured. +- [f32 conversions in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) (`v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_pknorm_i16/u16_f32`, `v_cvt_pk_u8_f32`, `v_cvt_pkrtz_f16_f32` and `v_cvt_f16_f32`) flush their f32 denormal inputs by bit 0 of the `FLOAT_MODE` f32 denormal field: mode 1 keeps them as mode 3 does, and mode 2 flushes them as mode 0 does. Measured on RDNA2 (gfx1036, ROCm 7.1.1, Linux 7.2) over 148 sources and 484 operand pairs (zeros, denormals, minimum normals, f16 limits, the largest finite value, infinities, NaNs) at IEEE=0 and IEEE=1 in every f32 denormal and round mode, and for the f16 and f64 results in every f16/f64 denormal and round mode. `v_div_scale_f32` and `v_div_fixup_f32` do not follow this rule and still throw in modes 1 and 2, as do `v_mad_mixlo/mixhi_f16`, which RDNA2 lacks. The PS5's gfx1013 is not measured. - [f16 compares](../../../core/shader/recompiler/Translation/src/CompareInstructions.cpp) (`v_cmp_X_f16` and `v_cmpx_X_f16` with X = eq, ge, gt, le, lg, lt, neq, nge, ngt, nle, nlg, nlt) flush their f16 denormal inputs to a zero of the same sign by bit 0 of the `FLOAT_MODE` f16 denormal field: modes 0 and 2 flush them, 1 and 3 keep them. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) over 256 operand pairs of zeros, denormals, minimum normals, ones, the largest finite value, infinities and quiet and signaling NaNs of both signs, in VOPC form, at IEEE=0 and IEEE=1 with each f16 denormal mode 0 to 3, f16 round to nearest even, DX10_CLAMP=1, f32 denormals kept and FP16_OVFL=0. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [DB_SHADER_CONTROL.PRIMITIVE_ORDERED_PIXEL_SHADER](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the host pixel interlock spans the whole shader, not only up to `s_sendmsg` ORDERED_PS_DONE. - [POPS collision SGPRs](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the collision wave ID and intrawave collision mask read 0, so a title skips its POPS wait and runs its per-collision loop once. From 55ac01bc1e393a1739d6e60046da293f41594dfe Mon Sep 17 00:00:00 2001 From: TechLevelZero <35297505+TechLevelZero@users.noreply.github.com> Date: Sat, 24 Oct 2026 16:28:03 +0100 Subject: [PATCH 223/428] test(agc): run v_cvt_f64_f32 apart from the f32 conversions in their denormal test The conversion test failed before checking anything on a device without shaderFloat64, as its single shader included v_cvt_f64_f32. That instruction now runs in a second shader only when the target has the Float64 capability, so the other f32 conversions are still checked on such devices. Co-Authored-By: Claude Opus 5.5 --- .../execution/F32ConvertDenormalModes.cpp | 57 ++++++++++++------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F32ConvertDenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32ConvertDenormalModes.cpp index 5720a9ddae..9dcc8b2bf9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F32ConvertDenormalModes.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32ConvertDenormalModes.cpp @@ -20,14 +20,21 @@ constexpr std::uint32_t Threads = 64; constexpr std::uint32_t Inputs = 4; constexpr std::uint32_t Results = 16; constexpr std::uint32_t Columns = 12; +constexpr std::uint32_t Float64Low = 5; +constexpr std::uint32_t Float64High = 6; alignas(256) std::array Input{}; alignas(256) std::array Output{}; -alignas(256) constexpr std::array Code{ +alignas(256) constexpr std::array Code{ 0x34020084u, 0x34040086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e141104u, 0x7e160f04u, 0x7e181904u, - 0x7e1a1b04u, 0x7e1c7f04u, 0x7e1e2104u, 0xd7680011u, 0x00020b04u, 0xd7690012u, 0x00020b04u, 0xd55e0013u, - 0x02010104u, 0x5e280b04u, 0x7e2a1504u, 0xe0781000u, 0x80010a02u, 0xe0781010u, 0x80010e02u, 0xe0781020u, - 0x80011202u, 0xbf810000u, + 0x7e1a1b04u, 0x7e1c7f04u, 0xd7680011u, 0x00020b04u, 0xd7690012u, 0x00020b04u, 0xd55e0013u, 0x02010104u, + 0x5e280b04u, 0x7e2a1504u, 0xe0781000u, 0x80010a02u, 0xe0781010u, 0x80010e02u, 0xe0781020u, 0x80011202u, + 0xbf810000u, +}; + +alignas(256) constexpr std::array Float64Code{ + 0x34020084u, 0x34040086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e1e2104u, 0xe0781000u, 0x80010a02u, + 0xe0781010u, 0x80010e02u, 0xe0781020u, 0x80011202u, 0xbf810000u, }; constexpr const char* Names[Columns] = { @@ -97,7 +104,11 @@ std::string Hex(std::uint32_t value) { return text; } -std::vector Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode floatMode) { +bool InPass(std::uint32_t column, bool float64) { + return (column == Float64Low || column == Float64High) == float64; +} + +std::vector Run(AgcDriver::VulkanDevice& device, bool float64, ShaderRecompiler::ShaderFloatMode floatMode) { Input.fill(0u); for (std::uint32_t lane = 0; lane < Vectors.size(); ++lane) { Input[lane * Inputs] = Vectors[lane].operands[0]; @@ -109,7 +120,7 @@ std::vector Run(AgcDriver::VulkanDevice& device, ShaderRecompiler const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); std::copy(input.begin(), input.end(), userData.begin()); std::copy(output.begin(), output.end(), userData.begin() + 4); - const std::span code(Code); + const auto code = float64 ? std::span(Float64Code) : std::span(Code); const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ @@ -131,9 +142,10 @@ std::string Where(std::uint32_t lane, std::uint32_t column, std::uint32_t denorm std::to_string(denormals) + (ieee ? ", IEEE=1" : ", IEEE=0"); } -void CheckFlushed(const std::vector& output, std::uint32_t denormals, bool ieee) { +void CheckFlushed(const std::vector& output, bool float64, std::uint32_t denormals, bool ieee) { for (std::uint32_t lane = 0; lane < Vectors.size(); ++lane) { for (std::uint32_t column = 0; column < Columns; ++column) { + if (!InPass(column, float64)) continue; const auto value = output[lane * Results + column]; const auto expected = Vectors[lane].denormalsFlushed[column]; Require(value == expected, Where(lane, column, denormals, ieee) + " is " + Hex(value) + ", expected " + Hex(expected)); @@ -141,9 +153,10 @@ void CheckFlushed(const std::vector& output, std::uint32_t denorm } } -void CheckKept(const std::vector& output, bool ieee) { +void CheckKept(const std::vector& output, bool float64, bool ieee) { for (std::uint32_t lane = 0; lane < Vectors.size(); ++lane) { for (std::uint32_t column = 0; column < Columns; ++column) { + if (!InPass(column, float64)) continue; const auto value = output[lane * Results + column]; const auto& vector = Vectors[lane]; Require(value == vector.denormalsKept[column] || value == vector.denormalsFlushed[column], @@ -153,9 +166,10 @@ void CheckKept(const std::vector& output, bool ieee) { } } -void CheckSame(const std::vector& output, const std::vector& reference, std::uint32_t denormals, std::uint32_t like, bool ieee) { +void CheckSame(const std::vector& output, const std::vector& reference, bool float64, std::uint32_t denormals, std::uint32_t like, bool ieee) { for (std::uint32_t lane = 0; lane < Vectors.size(); ++lane) { for (std::uint32_t column = 0; column < Columns; ++column) { + if (!InPass(column, float64)) continue; const auto value = output[lane * Results + column]; const auto expected = reference[lane * Results + column]; Require(value == expected, Where(lane, column, denormals, ieee) + " is " + Hex(value) + ", but mode " + std::to_string(like) + " gave " + Hex(expected)); @@ -173,16 +187,21 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - for (const bool ieee : {false, true}) { - const auto flushed = Run(*device, Mode(0u, ieee)); - CheckFlushed(flushed, 0u, ieee); - const auto inputsFlushed = Run(*device, Mode(2u, ieee)); - CheckFlushed(inputsFlushed, 2u, ieee); - CheckSame(inputsFlushed, flushed, 2u, 0u, ieee); - const auto kept = Run(*device, Mode(3u, ieee)); - CheckKept(kept, ieee); - const auto outputsFlushed = Run(*device, Mode(1u, ieee)); - CheckSame(outputsFlushed, kept, 1u, 3u, ieee); + const bool hasFloat64 = TargetHasCapability(device->Target(), spv::CapabilityFloat64); + if (!hasFloat64) std::puts("v_cvt_f64_f32 skipped, the device has no shaderFloat64"); + for (const bool float64 : {false, true}) { + if (float64 && !hasFloat64) continue; + for (const bool ieee : {false, true}) { + const auto flushed = Run(*device, float64, Mode(0u, ieee)); + CheckFlushed(flushed, float64, 0u, ieee); + const auto inputsFlushed = Run(*device, float64, Mode(2u, ieee)); + CheckFlushed(inputsFlushed, float64, 2u, ieee); + CheckSame(inputsFlushed, flushed, float64, 2u, 0u, ieee); + const auto kept = Run(*device, float64, Mode(3u, ieee)); + CheckKept(kept, float64, ieee); + const auto outputsFlushed = Run(*device, float64, Mode(1u, ieee)); + CheckSame(outputsFlushed, kept, float64, 1u, 3u, ieee); + } } std::puts("f32 conversion denormal mode tests passed"); return 0; From fe4e924a57ff0a7f9247f46580e33903f134d035 Mon Sep 17 00:00:00 2001 From: Dextroid17 <217507631+Dextroid17@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:35:29 -0400 Subject: [PATCH 224/428] fix(agc): keep UTF-8 text intact in trace summaries and traces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TraceFormat::AppendString escaped every byte at or above 0x7f on its own as \u00XX. A JSON \u escape names a code point, not a byte, so a UTF-8 title came back as different characters: PPSA26344's param.json name "Ghost of Yōtei" (ō = C5 8D) was written as \u00c5\u008d and reads back as "YÅ\x8dtei" in every summary line. A byte that starts a well-formed UTF-8 sequence (Unicode Table 3-7: no overlong forms, no surrogates, nothing above U+10FFFF) is now copied as is, since JSON text is UTF-8. Control characters, DEL and any byte outside a well-formed sequence are still escaped as before, so the existing lone-0xe9 case is unchanged. --- .../Execution/include/PerformanceTrace.hpp | 44 ++++++++++++++++++- .../tests/PerformanceTrace.cpp | 15 +++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp index e683bc7695..49858aaa31 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp @@ -451,18 +451,30 @@ public: static void AppendString(std::string& output, std::string_view text) { output += '"'; - for (const char character : text) { + std::size_t index = 0; + while (index < text.size()) { + const char character = text[index]; const auto byte = static_cast(character); if (character == '"' || character == '\\') { output += '\\'; output += character; - } else if (byte < 0x20 || byte >= 0x7f) { + } else if (byte >= 0x80) { + if (const auto length = utf8SequenceLength(text, index); length != 0) { + output.append(text.substr(index, length)); + index += length; + continue; + } + char escaped[8]; + std::snprintf(escaped, sizeof escaped, "\\u%04x", static_cast(byte)); + output += escaped; + } else if (byte < 0x20 || byte == 0x7f) { char escaped[8]; std::snprintf(escaped, sizeof escaped, "\\u%04x", static_cast(byte)); output += escaped; } else { output += character; } + ++index; } output += '"'; } @@ -697,6 +709,34 @@ public: } private: + static std::size_t utf8SequenceLength(std::string_view text, std::size_t index) { + const auto lead = static_cast(text[index]); + std::size_t length = 0; + unsigned char low = 0x80; + unsigned char high = 0xbf; + if (lead >= 0xc2 && lead <= 0xdf) { + length = 2; + } else if (lead >= 0xe0 && lead <= 0xef) { + length = 3; + if (lead == 0xe0) low = 0xa0; + if (lead == 0xed) high = 0x9f; + } else if (lead >= 0xf0 && lead <= 0xf4) { + length = 4; + if (lead == 0xf0) low = 0x90; + if (lead == 0xf4) high = 0x8f; + } else { + return 0; + } + if (text.size() - index < length) return 0; + for (std::size_t offset = 1; offset < length; ++offset) { + const auto next = static_cast(text[index + offset]); + if (next < low || next > high) return 0; + low = 0x80; + high = 0xbf; + } + return length; + } + static void AppendMachine(std::string& output, const TraceSummary& summary) { if (summary.host) { output += ",\"cpu\":"; diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp index efad88d641..a6272db6c6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp @@ -134,6 +134,20 @@ void EscapeStrings() { Check(output == "\"a\\\"b\\\\c\\u000a\\u00e9\"", "string escaping is not valid JSON"); } +void Utf8Strings() { + std::string output; + TraceFormat::AppendString(output, "Ghost of Y\xc5\x8dtei \xe2\x84\xa2 \xf0\x9f\x8e\xae"); + Check(output == "\"Ghost of Y\xc5\x8dtei \xe2\x84\xa2 \xf0\x9f\x8e\xae\"", "a UTF-8 title was not written as its own characters"); + output.clear(); + TraceFormat::AppendString(output, "\xc0\xaf\xed\xa0\x80\xf4\x90\x80\x80\xe2\x84\x7f\xc5"); + Check(output == "\"\\u00c0\\u00af\\u00ed\\u00a0\\u0080\\u00f4\\u0090\\u0080\\u0080\\u00e2\\u0084\\u007f\\u00c5\"", "a byte outside a valid UTF-8 sequence was not escaped"); + AgcDriver::TraceSummary summary; + summary.title = AgcDriver::TraceTitle{"PPSA26344", "Ghost of Y\xc5\x8dtei", "01.000.000"}; + const auto origin = Clock::now(); + const std::vector presents{origin, origin + milliseconds(16)}; + Check(TraceFormat::Summary(summary, {}, presents).find("\"title\":\"Ghost of Y\xc5\x8dtei\"") != std::string::npos, "the summary line did not keep a UTF-8 title"); +} + void EventsShape() { const auto origin = Clock::now(); const std::vector slices{ @@ -576,6 +590,7 @@ int main() { FullBufferEvents(); FoldShortSlices(); EscapeStrings(); + Utf8Strings(); EventsShape(); BatchIds(); SummaryFields(); From 7151c6c0907f7440c724c68652846df29d709e67 Mon Sep 17 00:00:00 2001 From: Bram910 Date: Sat, 24 Oct 2026 18:08:46 +0200 Subject: [PATCH 225/428] docs(relinker): clarify call-site analysis limits --- core/relinker/relinker/tests/CallSiteResolverTests.cpp | 2 +- docs/dev/debt/relinker.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/relinker/relinker/tests/CallSiteResolverTests.cpp b/core/relinker/relinker/tests/CallSiteResolverTests.cpp index 06fed60f07..16ca224079 100644 --- a/core/relinker/relinker/tests/CallSiteResolverTests.cpp +++ b/core/relinker/relinker/tests/CallSiteResolverTests.cpp @@ -41,7 +41,7 @@ int main() { check("BND NOTRACK indirect tail jump", {0xf2, 0x3e, 0xff, 0x25, 0xf8, 0, 0, 0}, true); check("BND REX indirect call", {0xf2, 0x48, 0xff, 0x15, 0xf8, 0, 0, 0}, true); check("NOTRACK REX indirect tail jump", {0x3e, 0x48, 0xff, 0x25, 0xf8, 0, 0, 0}, true); - check("DS-prefixed pointer load", {0x3e, 0x48, 0x8b, 0x05, 0xf8, 0, 0, 0}, true); + check("NOTRACK-prefixed pointer load", {0x3e, 0x48, 0x8b, 0x05, 0xf8, 0, 0, 0}, true); check("REPNE-prefixed pointer load", {0xf2, 0x48, 0x8b, 0x05, 0xf8, 0, 0, 0}, true); check("DS after REX.W leaves a narrow load", {0x48, 0x3e, 0x8b, 0x05, 0xf8, 0, 0, 0}, false); check("REPNE after REX.W leaves a narrow load", {0x48, 0xf2, 0x8b, 0x05, 0xf8, 0, 0, 0}, false); diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 3eccf99c51..0d6250f0ce 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -10,6 +10,7 @@ - [Runtime-loaded modules the executable needs](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--module-dir`) are mapped at startup on Windows and started when the title loads them with `sceKernelLoadStartModule`, with their start arguments, as a Unity player loads `mono-ps4.sprx`. A title that calls such a module before loading it finds its initializers not run; whether the console starts it earlier is not measured. A module the executable only imports from and the title never loads itself gets no `module_start` and no initializers at all under this deferral, and nothing reports it. macOS still starts it at startup. - The executable file that [relinker](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens a console when launched, unless it is relinked with `--windows-gui`, which selects the GUI subsystem. A GUI build started from Explorer has no standard output or error handle; the [entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) gives each missing one `anyps5.log` beside the executable, replacing the previous run's file (#3282). Errors about the executable path or about waiting for the previous LoadExec process are written before the log is opened and are lost when the standard error handle is missing. If the log cannot be created, output to the missing handles is lost. - [Guest import registries](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - declared host imports of bundled modules are recorded with their module name and relocation target, even when another guest module exports the same stripped NID. No-op relocations are omitted. Registries describe import bindings, not implementation correctness or runtime reachability. +- Registry [call-site analysis](../../../core/relinker/relinker/src/analysis/CallSiteResolver.cpp) does not classify instructions with `F3`, `66`, `67`, `F0`, `2E`, `26`, `36`, FS/GS prefixes, or `8B` loads without REX.W, so missing call sites do not establish that an import is unused. - Converted Windows main executables [omit `DYNAMIC_BASE`](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) by default, not only with `--windows-fixed-base`: on the console the executable lies inside the 40-bit window that the title's own `sceLibcMspaceCreate` checks (#3043). Relocations are kept, so if `0x140000000` is taken Windows relocates the image, possibly above that window. Verified only on Windows 10. - [SDK memory-management syscall traps](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) under `--trap-sdk-syscall-stubs` recognize three exact instruction sequences from [ps5-payload-dev SDK libc/mman.c](https://github.com/ps5-payload-dev/sdk/blob/12cf3dda40/libc/mman.c), observed in Nuvio Maxd 0.5.15 (PPSA99288): syscall 74 and the two syscall 477 paths, with their argument preparation and carry-flag error handling. Matching requires a decoded instruction boundary and the complete sequence inside the scanned code range. Other SDK/compiler output remains rejected. The matched syscall becomes `UD2`; this does not implement mmap or mprotect and a call still faults. - The [instruction decoder](../../../core/relinker/codegen/src/x86/X64InstructionDecoder.cpp) reads a 16-bit displacement for `66 E8`, `66 E9` and `66 0F 80`-`8F` without REX.W, as AMD processors, the console included, do (binutils `objdump -M amd64`; REX.W selects the 32-bit form). AMD also truncates the target of every near relative branch with that prefix, rel8, LOOP and JRCXZ forms included, to 16 bits. Instruction metadata marks these branches as truncated and reports no branch target for them, and stubs refuse to move them. Compilers do not emit these forms. From 995958eb978ee804b81148d5007588115e08ebd5 Mon Sep 17 00:00:00 2001 From: Bram910 Date: Sat, 24 Oct 2026 18:13:45 +0200 Subject: [PATCH 226/428] test(tools): cover Unicode ZIP path rewrites --- tools/tests/test_build_web.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tools/tests/test_build_web.py b/tools/tests/test_build_web.py index 455da60cc9..78cdb95d0e 100644 --- a/tools/tests/test_build_web.py +++ b/tools/tests/test_build_web.py @@ -5,12 +5,14 @@ import io import json from pathlib import Path import subprocess +import struct import sys import tempfile import unittest from unittest.mock import patch import warnings import zipfile +import zlib sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import build_web @@ -26,6 +28,26 @@ def library_zip(entries): class LibraryPublicationTests(unittest.TestCase): + def test_unicode_path_rewrite_is_rejected_before_decompression(self): + original = "libs/../libc.prx" + rewritten = "libs/libc.prx" + entry = zipfile.ZipInfo(original) + unicode_path = b"\x01" + struct.pack(" Date: Sat, 24 Oct 2026 18:20:07 +0200 Subject: [PATCH 227/428] fix(tools): use UTF-8 for patch advisor I/O --- tools/suggest_patches.py | 6 +++++- tools/tests/test_suggest_patches.py | 33 +++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/tools/suggest_patches.py b/tools/suggest_patches.py index 51d91d85a1..ead6b408e7 100644 --- a/tools/suggest_patches.py +++ b/tools/suggest_patches.py @@ -69,6 +69,9 @@ def validate_entry(directory, entry): def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser(description="Recommend a local patch only after metadata, hashes and relinker verification match") parser.add_argument("--catalog", required=True, type=Path, help="directory containing index.json and patch files") parser.add_argument("--param-json", required=True, type=Path, help="title metadata file") @@ -135,7 +138,8 @@ def main(argv=None): command.extend(("--executable", executable)) command.append(str(args.input)) try: - result = subprocess.run(command, capture_output=True, text=True, timeout=args.timeout_seconds) + result = subprocess.run(command, capture_output=True, text=True, encoding="utf-8", errors="replace", + timeout=args.timeout_seconds) except subprocess.TimeoutExpired: print(f"{label}: rejected: relinker verification exceeded {args.timeout_seconds} seconds") continue diff --git a/tools/tests/test_suggest_patches.py b/tools/tests/test_suggest_patches.py index 0da25c6f5d..0d892bb7f9 100644 --- a/tools/tests/test_suggest_patches.py +++ b/tools/tests/test_suggest_patches.py @@ -63,6 +63,39 @@ class PatchAdvisorTests(unittest.TestCase): str(self.input)]) self.assertFalse((self.root / "relinker-patches.toml").exists()) + def test_relinker_output_is_utf8_independent_of_host_encoding(self): + actual_run = subprocess.run + for diagnostic in ("caf\u00e9 \u65e5\u672c\u8a9e".encode(), b"invalid byte \xff"): + with self.subTest(diagnostic=diagnostic): + stdout = VERIFIED.encode() + diagnostic + b"\n" + stderr = diagnostic + b"\n" + captured = [] + + def verify(command, **options): + script = f"import os; os.write(1, {stdout!r}); os.write(2, {stderr!r})" + result = actual_run([sys.executable, "-c", script], **options) + captured.append(result) + return result + + with patch.object(suggest_patches.subprocess, "_text_encoding", return_value="ascii"), \ + patch.object(suggest_patches.subprocess, "run", side_effect=verify): + self.assertEqual(self.advise(), 0) + self.assertEqual(captured[0].stdout, stdout.decode("utf-8", errors="replace")) + self.assertEqual(captured[0].stderr, stderr.decode("utf-8", errors="replace")) + + @patch.object(suggest_patches.subprocess, "run") + def test_output_streams_use_utf8(self, run): + stdout = io.TextIOWrapper(io.BytesIO(), encoding="ascii") + stderr = io.TextIOWrapper(io.BytesIO(), encoding="ascii") + self.addCleanup(stdout.close) + self.addCleanup(stderr.close) + run.return_value = subprocess.CompletedProcess([], 0, VERIFIED, "") + with patch.object(suggest_patches.sys, "stdout", stdout), patch.object(suggest_patches.sys, "stderr", stderr): + self.assertEqual(self.advise(), 0) + for stream in (stdout, stderr): + self.assertEqual(stream.encoding, "utf-8") + self.assertEqual(stream.errors, "backslashreplace") + @patch.object(suggest_patches.subprocess, "run") def test_changed_module_is_not_sent_to_relinker(self, run): self.module.write_bytes(b"module version two") From dcb04d0aa14d81da2e78f176a385d076db92e3b6 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Sat, 24 Oct 2026 19:09:07 +0300 Subject: [PATCH 228/428] perf(agc): avoid rescanning previewed dispatch ranges and redundant compute prebuilds --- .../Execution/include/VulkanDevice.hpp | 1 + .../src/Driver/Dispatch/Dispatch.cpp | 2 +- .../src/Driver/Dispatch/DispatchAhead.cpp | 167 +++++++++++------- .../Execution/src/VulkanDevice.cpp | 13 +- 4 files changed, 110 insertions(+), 73 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index b80c032917..fa264da825 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -58,6 +58,7 @@ public: // Builds the pipeline of a journaled compute result ahead of its first dispatch (the journal // replay's hook, on its threads): false when the device already holds a pipeline for the module. bool ReplayComputePipeline(const ShaderRecompiler::RecompileResult& shader, bool pushConstants); + bool HasComputePipeline(const ShaderRecompiler::RecompileResult& shader) const; void PrebuildComputePipeline(const ShaderRecompiler::RecompileResult& shader, std::uint64_t programAddress = 0); // Starts the journal replay if the device has not (a headless device starts it at its first // draw or dispatch) and joins it (tests; the teardown joins it anyway). diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp index ea10e8e1f5..3876ef6f6e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -414,7 +414,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, parked.resolvedAt = std::chrono::steady_clock::now(); ++groupStats.resolved; if (cached) ++groupStats.resolvedHits; - if (!cached && compiledResult != nullptr) { + if (!cached && compiledResult != nullptr && !localDevice->HasComputePipeline(*compiledResult)) { SubmitLibraryPrebuild([localDevice, compiledResult, address] { try { localDevice->PrebuildComputePipeline(*compiledResult, address); } catch (const std::exception&) {} }); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp index cb35e49da2..0ebd368419 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp @@ -42,7 +42,17 @@ std::uint64_t Driver::DispatchesAdoptedAhead() const { return dispatchesAdoptedAhead.load(std::memory_order_relaxed); } +namespace { + +std::size_t& PreviewedUntil() { + static thread_local std::size_t until = 0; + return until; +} + +} + void Driver::clearResolvedAhead() { + PreviewedUntil() = 0; auto& resolved = resolvedAhead(); if (resolved.empty()) return; groupCaptureStats().stale += resolved.size(); @@ -129,85 +139,108 @@ void Driver::resolveGroupAhead(const Submission& submission, const QueueState& l } } resolving; const auto& commands = submission.commands; - if (submission.shaders != nullptr) { - QueueState preview = live; - std::vector pendingShaders; - std::vector submittedShaders; - for (std::size_t scan = from; scan < commands.size();) { - const auto header = commands[scan]; + if (submission.shaders != nullptr && from >= PreviewedUntil()) { + bool hasFurtherDispatch = false; + std::size_t limit = from < commands.size() ? from + Pm4::PacketWords(commands[from]) : commands.size(); + while (limit < commands.size()) { + const auto header = commands[limit]; if (Pm4::FillerPacket(header)) { - ++scan; + ++limit; continue; } const auto count = Pm4::PacketWords(header); - if (scan + count > commands.size()) break; - const auto packet = std::span(commands).subspan(scan, count); + if (limit + count > commands.size()) break; const auto opcode = (header >> 8u) & 0xffu; - const auto next = scan + count; if (header == FlipPacketHeader || header == RenderingWaitPacketHeader || opcode == 0x22 || opcode == 0x3f || opcode == 0x3c || opcode == 0x93) break; - if (opcode == 0x15 || opcode == 0x16) { - if (count < (opcode == 0x15 ? 5u : 3u)) break; - const auto low = preview.shader.find(0x20c); - const auto high = preview.shader.find(0x20d); - if (low != preview.shader.end() && high != preview.shader.end()) { - const auto address = (static_cast(low->second) << 8u) | (static_cast(high->second & 0xffu) << 40u); - if (auto registered = RegisteredProgram(*submission.shaders, address, {0}); registered != nullptr && registered->type == 0) { - bool isPending = false; - bool isEmpty = false; - { - std::lock_guard lock(registered->prepared->mutex); - isPending = registered->prepared->pending; - isEmpty = registered->prepared->entries.empty(); - } - if (isPending) { - if (std::ranges::find(pendingShaders, registered->prepared.get()) == pendingShaders.end()) { - pendingShaders.push_back(registered->prepared.get()); - } - } else if (isEmpty && std::ranges::find(submittedShaders, registered->prepared.get()) == submittedShaders.end()) { - const auto codeOffset = static_cast((address - registered->codeAddress) / sizeof(std::uint32_t)); - if (codeOffset < registered->code.size()) { - try { - auto compute = Graphics::DecodeComputeStageInfo(preview.shader, registered->header); - submittedShaders.push_back(registered->prepared.get()); - const auto rsrc2 = preview.shader.find(0x213); - const auto userCount = rsrc2 != preview.shader.end() ? ((rsrc2->second >> 1u) & 0x1fu) : 0u; - std::vector userData(userCount, 0u); - for (std::uint32_t i = 0; i < userCount; ++i) { - if (const auto reg = preview.shader.find(0x240u + i); reg != preview.shader.end()) userData[i] = reg->second; + if (opcode == 0x15 || opcode == 0x16) hasFurtherDispatch = true; + limit += count; + } + PreviewedUntil() = limit; + if (hasFurtherDispatch) { + static thread_local QueueState* previewSlot = nullptr; + auto& preview = ShaderRecompiler::ThreadOwned(previewSlot); + preview.shader = live.shader; + preview.predication = live.predication; + std::vector pendingShaders; + std::vector submittedShaders; + std::size_t scan = from; + while (scan < limit) { + const auto header = commands[scan]; + if (Pm4::FillerPacket(header)) { + ++scan; + continue; + } + const auto count = Pm4::PacketWords(header); + if (scan + count > limit) break; + const auto packet = std::span(commands).subspan(scan, count); + const auto opcode = (header >> 8u) & 0xffu; + const auto next = scan + count; + if (opcode == 0x15 || opcode == 0x16) { + if (count < (opcode == 0x15 ? 5u : 3u)) break; + if (scan > from) { + const auto low = preview.shader.find(0x20c); + const auto high = preview.shader.find(0x20d); + if (low != preview.shader.end() && high != preview.shader.end()) { + const auto address = (static_cast(low->second) << 8u) | (static_cast(high->second & 0xffu) << 40u); + if (auto registered = RegisteredProgram(*submission.shaders, address, {0}); registered != nullptr && registered->type == 0) { + bool isPending = false; + bool isEmpty = false; + { + std::lock_guard lock(registered->prepared->mutex); + isPending = registered->prepared->pending; + isEmpty = registered->prepared->entries.empty(); + } + if (isPending) { + if (std::ranges::find(pendingShaders, registered->prepared.get()) == pendingShaders.end()) { + pendingShaders.push_back(registered->prepared.get()); } - const auto wave = (packet.back() & 0x8000u) != 0 ? 32u : 64u; - SubmitRegistrationPreparation([registered, localDevice, address, codeOffset, wave, compute, userData = std::move(userData), state = preview.shader]() mutable { - std::vector memory{{registered->codeAddress, std::as_bytes(std::span(registered->code))}}; - if (!registered->header.empty()) memory.push_back({registered->headerAddress, registered->header}); - ShaderRecompiler::RecompileRequest request{ - {ShaderRecompiler::ShaderStage::Compute, address, std::span(registered->code).subspan(codeOffset), registered->headerAddress, registered->header}, - {wave, 0, userData, compute, std::nullopt, std::nullopt, memory, DispatchFloatMode(*registered, state)}, - localDevice->ComputeTarget(wave), - {0, 0, 0, 128} - }; - try { static_cast(InvocationFor(*registered, codeOffset, request)); } catch (const std::exception&) {} - }, registered->prepared.get()); - } catch (const std::exception&) {} + } else if (isEmpty && std::ranges::find(submittedShaders, registered->prepared.get()) == submittedShaders.end()) { + const auto codeOffset = static_cast((address - registered->codeAddress) / sizeof(std::uint32_t)); + if (codeOffset < registered->code.size()) { + try { + auto compute = Graphics::DecodeComputeStageInfo(preview.shader, registered->header); + submittedShaders.push_back(registered->prepared.get()); + const auto rsrc2 = preview.shader.find(0x213); + const auto userCount = rsrc2 != preview.shader.end() ? ((rsrc2->second >> 1u) & 0x1fu) : 0u; + std::vector userData(userCount, 0u); + for (std::uint32_t i = 0; i < userCount; ++i) { + if (const auto reg = preview.shader.find(0x240u + i); reg != preview.shader.end()) userData[i] = reg->second; + } + const auto wave = (packet.back() & 0x8000u) != 0 ? 32u : 64u; + SubmitRegistrationPreparation([registered, localDevice, address, codeOffset, wave, compute, userData = std::move(userData), state = preview.shader]() mutable { + std::vector memory{{registered->codeAddress, std::as_bytes(std::span(registered->code))}}; + if (!registered->header.empty()) memory.push_back({registered->headerAddress, registered->header}); + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::ShaderStage::Compute, address, std::span(registered->code).subspan(codeOffset), registered->headerAddress, registered->header}, + {wave, 0, userData, compute, std::nullopt, std::nullopt, memory, DispatchFloatMode(*registered, state)}, + localDevice->ComputeTarget(wave), + {0, 0, 0, 128} + }; + try { static_cast(InvocationFor(*registered, codeOffset, request)); } catch (const std::exception&) {} + }, registered->prepared.get()); + } catch (const std::exception&) {} + } + } } } } + Pm4::NoteDispatchRegisters(packet, preview); + } else if (Pm4::IndirectRegisterOpcode(opcode)) { + const auto found = submission.registerLists.find(scan); + if (count != 5 || found == submission.registerLists.end() || found->second.size() != static_cast(packet[4]) * 2 || !Pm4::IndirectRegistersValid(found->second)) break; + Pm4::ExecuteIndirectRegisters(packet, found->second, preview); + } else if (!Pm4::AccessesMemory(header) && opcode != 0x3c && opcode != 0x93 && opcode != 0x37 && opcode != 0x42 && opcode != 0x46 && opcode != 0x49 && opcode != 0x58) { + try { + Pm4::Execute(packet, preview); + } catch (const std::exception&) { + break; + } } - Pm4::NoteDispatchRegisters(packet, preview); - } else if (Pm4::IndirectRegisterOpcode(opcode)) { - const auto found = submission.registerLists.find(scan); - if (count != 5 || found == submission.registerLists.end() || found->second.size() != static_cast(packet[4]) * 2 || !Pm4::IndirectRegistersValid(found->second)) break; - Pm4::ExecuteIndirectRegisters(packet, found->second, preview); - } else if (!Pm4::AccessesMemory(header) && opcode != 0x3c && opcode != 0x93 && opcode != 0x37 && opcode != 0x42 && opcode != 0x46 && opcode != 0x49 && opcode != 0x58) { - try { - Pm4::Execute(packet, preview); - } catch (const std::exception&) { - break; - } + scan = next; + } + for (auto it = pendingShaders.rbegin(); it != pendingShaders.rend(); ++it) { + PrioritizeRegistrationPreparation(**it); } - scan = next; - } - for (auto it = pendingShaders.rbegin(); it != pendingShaders.rend(); ++it) { - PrioritizeRegistrationPreparation(**it); } } for (std::size_t cursor = from; cursor < commands.size();) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 6776ea1ab9..8b1466db5e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2294,6 +2294,13 @@ bool VulkanDevice::ReplayComputePipeline(const ShaderRecompiler::RecompileResult return state->computePipelines.try_emplace(key, std::move(objects)).second; } +bool VulkanDevice::HasComputePipeline(const ShaderRecompiler::RecompileResult& shader) const { + const std::array shaders{{{ShaderRecompiler::ShaderStage::Compute, &shader, 0}}}; + const auto key = ComputePipelineKey::Of(shader, Graphics::PushConstantStages(shaders) != 0); + std::lock_guard pipelines(state->computePipelinesMutex); + return !key.identity.Empty() && (state->computePipelines.contains(key) || state->computePipelinesBuilding.contains(key)); +} + void VulkanDevice::PrebuildComputePipeline(const ShaderRecompiler::RecompileResult& shader, std::uint64_t programAddress) { if (shader.spirv.size() < 5 || shader.spirv[0] != 0x07230203u) return; ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion); @@ -2302,11 +2309,7 @@ void VulkanDevice::PrebuildComputePipeline(const ShaderRecompiler::RecompileResu const std::array shaders{{{ShaderRecompiler::ShaderStage::Compute, &shader, 0}}}; const auto pushStages = Graphics::PushConstantStages(shaders); if (pushStages != 0 && state->properties.limits.maxPushConstantsSize < Graphics::ComputePushConstantBytes) return; - const auto key = ComputePipelineKey::Of(shader, pushStages != 0); - if (!key.identity.Empty()) { - std::lock_guard pipelines(state->computePipelinesMutex); - if (state->computePipelines.contains(key) || state->computePipelinesBuilding.contains(key)) return; - } + if (HasComputePipeline(shader)) return; const auto layouts = Graphics::DescribeSetLayouts(context, shaders); static_cast(computePipeline(context, shader, layouts.layouts.main, layouts.mainKey, pushStages, programAddress)); } From 4f3e5fc60f4e450e92122a4c505dbcf28b075d2c Mon Sep 17 00:00:00 2001 From: plexerPK <160128888+plexerPK@users.noreply.github.com> Date: Sat, 24 Oct 2026 12:43:50 +0200 Subject: [PATCH 229/428] fix(tools): report invalid release zip archives as CLI errors --- tools/build_web.py | 2 +- tools/tests/test_web_release.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/tools/build_web.py b/tools/build_web.py index 8a5e555bf0..8bf9dd9af6 100644 --- a/tools/build_web.py +++ b/tools/build_web.py @@ -185,7 +185,7 @@ def main(argv=None): copy_sources(output) print(output) return 0 - except (OSError, ValueError) as error: + except (OSError, ValueError, zipfile.BadZipFile) as error: parser.error(str(error)) diff --git a/tools/tests/test_web_release.py b/tools/tests/test_web_release.py index d67632e117..781df3171e 100644 --- a/tools/tests/test_web_release.py +++ b/tools/tests/test_web_release.py @@ -212,6 +212,27 @@ class WebReleaseTests(unittest.TestCase): build_web.main() self.assertEqual((self.output / "native/relinker_wasm.mjs").read_bytes(), self.contents["native/relinker_wasm.mjs"]) + def test_invalid_zip_reports_cli_error_and_preserves_previous_site(self): + self.seed_previous() + previous = self.snapshot() + for name in ("web-v1.zip", "prx-windows-v1.zip", "prx-linux-v1.zip"): + with self.subTest(asset=name): + self.requests.clear() + original = self.responses[f"https://example.test/{name}"] + self.set_asset(name, original[:-22]) + errors = io.StringIO() + try: + with patch.object(build_web, "request", self.request), contextlib.redirect_stderr(errors): + with self.assertRaises(SystemExit) as error: + build_web.main(["--build", str(self.build), "--release"]) + self.assertEqual(error.exception.code, 2) + self.assertIn("error: File is not a zip file", errors.getvalue()) + self.assertNotIn("Traceback", errors.getvalue()) + self.assertEqual(self.snapshot(), previous) + self.assertEqual(list(self.build.glob(".web-release-*")), []) + finally: + self.set_asset(name, original) + def test_local_build_still_copies_sources_without_network(self): source = self.root / "source/demo/web" source.mkdir(parents=True) From c32942496d945a9558b3f3fce10be6193dcba8fe Mon Sep 17 00:00:00 2001 From: plexerPK <160128888+plexerPK@users.noreply.github.com> Date: Sat, 24 Oct 2026 19:03:00 +0200 Subject: [PATCH 230/428] fix(tools): name malformed release archives in CLI errors --- tools/build_web.py | 6 +++++- tools/tests/test_web_release.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tools/build_web.py b/tools/build_web.py index 8bf9dd9af6..9e141735b0 100644 --- a/tools/build_web.py +++ b/tools/build_web.py @@ -90,6 +90,8 @@ def publish_libraries(output, release=None): path = f"libraries/{platform}-{checksum}.zip" temporary.replace(output / path) manifest["platforms"][platform] = {"path": path, "size": size, "expandedBytes": expanded, "sha256": checksum} + except zipfile.BadZipFile as error: + raise ValueError(f"Invalid release archive {asset['name']}: {error}") from error finally: temporary.unlink(missing_ok=True) (output / "release.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") @@ -158,6 +160,8 @@ def publish_release(output): raise RuntimeError(f"Could not restore release output. Previous site is preserved at {previous}") from error raise published = True + except zipfile.BadZipFile as error: + raise ValueError(f"Invalid release archive {asset['name']}: {error}") from error finally: if published or not previous.exists(): shutil.rmtree(temporary) @@ -185,7 +189,7 @@ def main(argv=None): copy_sources(output) print(output) return 0 - except (OSError, ValueError, zipfile.BadZipFile) as error: + except (OSError, ValueError) as error: parser.error(str(error)) diff --git a/tools/tests/test_web_release.py b/tools/tests/test_web_release.py index 781df3171e..6675c59869 100644 --- a/tools/tests/test_web_release.py +++ b/tools/tests/test_web_release.py @@ -226,7 +226,7 @@ class WebReleaseTests(unittest.TestCase): with self.assertRaises(SystemExit) as error: build_web.main(["--build", str(self.build), "--release"]) self.assertEqual(error.exception.code, 2) - self.assertIn("error: File is not a zip file", errors.getvalue()) + self.assertIn(f"error: Invalid release archive {name}: File is not a zip file", errors.getvalue()) self.assertNotIn("Traceback", errors.getvalue()) self.assertEqual(self.snapshot(), previous) self.assertEqual(list(self.build.glob(".web-release-*")), []) From 29fb9be1e321fd7ab3b3b12caf00fef283a520a6 Mon Sep 17 00:00:00 2001 From: hyvvs Date: Sat, 24 Oct 2026 19:08:15 +0200 Subject: [PATCH 231/428] fix(ci): preserve device reasons in CTest skip reports --- tools/check_skips.py | 13 ++++--- tools/tests/test_check_skips.py | 65 +++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/tools/check_skips.py b/tools/check_skips.py index 6ce7a7d47b..e2f8f246f0 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -11,12 +11,13 @@ NO_DISPLAY = re.compile(r"skipped,.*no display", re.IGNORECASE) NO_VULKAN_DEVICE = re.compile(r"skipped,.*no usable Vulkan device", re.IGNORECASE) HIGH_REFRESH_LIMITATION = re.compile(r"current display does not provide 120 Hz", re.IGNORECASE) +CTEST_SKIP_STATUS = re.compile(r"SKIP_RETURN_CODE=\d+|SKIP_REGULAR_EXPRESSION_MATCHED", re.IGNORECASE) WINDOWS_PREFIX = "agc" def device_limitations(output, patterns): - reasons = [line.strip() for line in output.splitlines() if line.strip().lower().startswith("skipped,")] + reasons = [match.group(0) for line in output.splitlines() if (match := re.search(r"skipped,.*", line, re.IGNORECASE))] return bool(reasons) and all(any(pattern.search(reason) for pattern in patterns) for reason in reasons) @@ -24,8 +25,8 @@ def high_refresh_limitation(output): reasons = [] for line in output.splitlines(): reason = line.strip() - if reason.lower().startswith("skipped,"): - reason = reason[len("skipped,"):].strip() + if match := re.search(r"skipped,", reason, re.IGNORECASE): + reason = reason[match.end():].strip() if reason: reasons.append(reason) return bool(reasons) and all(HIGH_REFRESH_LIMITATION.fullmatch(reason) for reason in reasons) @@ -44,7 +45,8 @@ def skips(path): name = case.get("name") if not name: raise ValueError(f"{path}: skipped testcase has no name") - output = ["skipped, " + reason.strip() for reason in (skipped.get("message", ""), skipped.text or "") if reason.strip()] + output = ["skipped, " + reason.strip() for reason in (skipped.get("message", ""), skipped.text or "") + if reason.strip() and not CTEST_SKIP_STATUS.fullmatch(reason.strip())] output += [case.findtext(channel) or "" for channel in ("system-out", "system-err")] yield name, "\n".join(output) @@ -64,7 +66,8 @@ def main(argv=None): parser.error(str(error)) if args.list: for name, output in found: - reason = next((line.strip() for line in output.splitlines() if line.strip().lower().startswith("skipped")), "") + reason = next((match.group(0) for line in output.splitlines() + if (match := re.search(r"skipped,.*", line, re.IGNORECASE))), "") print(f"{name}: {reason}") return 0 if args.job == "windows": diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index 02caf7fa3c..9c708417ee 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -37,6 +37,71 @@ OTHER = [ class CheckSkipsTests(unittest.TestCase): + def test_ctest_status_and_prefixed_device_reasons_are_expected(self): + reports = ( + ('skipped, the device lacks geometryShader', "linux"), + ('f16 native: skipped, the device has no native f16 with RTE rounding and kept denormals', "linux"), + ('video: skipped, no display or Vulkan device: ErrorInitializationFailed', "windows"), + ('skipped, no usable Vulkan device: none', "windows"), + ) + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + for report, job in reports: + with self.subTest(job=job, report=report): + junit.write_text(f'{report}', encoding="utf-8") + result = subprocess.run([sys.executable, "-I", str(TOOL), str(junit), "--job", job], + capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("all expected", result.stdout) + + def test_ctest_status_does_not_allow_missing_or_unexpected_reasons(self): + reports = ( + '', + '', + 'skipped, flaky\nskipped, the device lacks geometryShader', + ) + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + for report in reports: + for job in ("linux", "windows"): + with self.subTest(job=job, report=report): + junit.write_text(f'{report}', encoding="utf-8") + result = subprocess.run([sys.executable, "-I", str(TOOL), str(junit), "--job", job], + capture_output=True, text=True) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("error: guest_probe skipped", result.stdout) + + def test_ctest_status_preserves_high_refresh_policy(self): + reason = "skipped, current display does not provide 120 Hz" + reports = ( + (f'{reason}', 0), + (f'{reason}', 0), + (f'{reason}\nunexpected failure detail', 1), + (f'{reason}\nskipped, flaky', 1), + (f'skipped, no display\n{reason}', 1), + ) + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + for report, expected_code in reports: + for job in ("linux", "windows"): + with self.subTest(job=job, report=report): + junit.write_text(f'{report}', encoding="utf-8") + result = subprocess.run([sys.executable, "-I", str(TOOL), str(junit), "--job", job], + capture_output=True, text=True) + self.assertEqual(result.returncode, expected_code, result.stdout + result.stderr) + + def test_list_reports_device_reason_after_ctest_status(self): + xml = ('' + 'f16 native: skipped, the device has no native f16') + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + junit.write_text(xml, encoding="utf-8") + result = subprocess.run([sys.executable, "-I", str(TOOL), str(junit), "--list"], + capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("guest_probe: skipped, the device has no native f16", result.stdout) + self.assertNotIn("SKIP_RETURN_CODE", result.stdout) + def test_unexpected_skip_reason_cannot_hide_behind_a_device_limitation(self): reports = ( ('skipped, the device lacks geometryShader', "linux"), From 73a681b4883d3173f1f1cb5ed85cb50cdadf1d15 Mon Sep 17 00:00:00 2001 From: Ahmad Mujahid Date: Sat, 24 Oct 2026 21:28:35 +0400 Subject: [PATCH 232/428] fix(relinker): reject --frame-red-zone with --verify-patches --- core/relinker/cli/src/CliArgs.cpp | 2 +- core/relinker/cli/tests/CliArgsTests.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/relinker/cli/src/CliArgs.cpp b/core/relinker/cli/src/CliArgs.cpp index 47166d9972..6ab190a572 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -22,7 +22,7 @@ namespace { constexpr std::string_view kVerifyRejectedOptions[] = { "--windows", "--macos", "--windows-gui", "--windows-diagnostics", "--windows-fixed-base", "--trace-imports", "--linux-fixed-base", "--macos-fixed-base", - "--to-intel", "--to-rosetta", "--skip-syscall-check", "--trap-sdk-syscall-stubs", "unused-filter=", + "--to-intel", "--to-rosetta", "--frame-red-zone", "--skip-syscall-check", "--trap-sdk-syscall-stubs", "unused-filter=", "--rpath", "--registry", "--lazy-binding", "--autorun", "--patches", "--no-patches", }; diff --git a/core/relinker/cli/tests/CliArgsTests.cpp b/core/relinker/cli/tests/CliArgsTests.cpp index 8f18c4c963..3cd3069dc1 100644 --- a/core/relinker/cli/tests/CliArgsTests.cpp +++ b/core/relinker/cli/tests/CliArgsTests.cpp @@ -322,7 +322,7 @@ void verifyMode() { expectThrows({"--verify-patches", "p.toml", "--verify-output", "out.elf", "--exclude-sce-module", "x.prx"}, noEffect); expectThrows({"--verify-patches", "p.toml", "--verify-output", "out.elf", "--skip-sce-module"}, noEffect); expectThrows({"--verify-patches", "p.toml", "in.elf", "extra"}, "unexpected argument: extra"); - for (const auto* option : {"--windows", "--macos", "--to-intel", "--to-rosetta", "--trace-imports", "--lazy-binding", "--autorun", "--registry", "--skip-syscall-check", "--no-patches"}) + for (const auto* option : {"--windows", "--macos", "--to-intel", "--to-rosetta", "--frame-red-zone", "--trace-imports", "--lazy-binding", "--autorun", "--registry", "--skip-syscall-check", "--no-patches"}) expectThrows({"--verify-patches", "p.toml", option, "in.elf"}, std::string(option) + " cannot be used with --verify-patches"); expectThrows({"--verify-patches", "p.toml", "--rpath", "x", "in.elf"}, "--rpath cannot be used with --verify-patches"); expectThrows({"--verify-patches", "p.toml", "--patches", "q.toml", "in.elf"}, "--patches cannot be used with --verify-patches"); From 49b68167fadc1075c253c5b561651819ce9db497 Mon Sep 17 00:00:00 2001 From: Shahul Mubeen A <174251444+shahulmubeen-a@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:38:12 +0530 Subject: [PATCH 233/428] feat(agc): skip the draws and dispatches of the programs listed in a file APS5_SKIP_SHADER_FILE names a file of hex program addresses (whitespace separated, # starts a comment). Draws and dispatches that use a listed program are dropped as with APS5_SKIP_SHADER. The file is re-read within 500 ms of a change, so passes can be switched off and on while the title runs. Co-Authored-By: Claude Opus 5.5 --- .../Execution/include/Driver/Diagnostics.hpp | 2 +- .../Execution/src/Driver/Diagnostics.cpp | 54 +++++++++- .../src/Driver/Dispatch/Dispatch.cpp | 2 +- .../Execution/src/Driver/Draw/Draw.cpp | 2 +- .../tests/execution/SkipShaderFile.cpp | 100 ++++++++++++++++++ docs/dev/ENVIRONMENT.md | 3 +- 6 files changed, 156 insertions(+), 7 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/SkipShaderFile.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp index 39bb6c35c9..86505f5a7b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp @@ -10,7 +10,7 @@ namespace AgcDriver::DriverDetail { double TraceMs(); -std::uint64_t SkippedShader(); +bool SkippedShader(std::uint64_t address); class RefusedDraw final : public std::runtime_error { public: diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp index b94858e431..56cf447c34 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp @@ -1,7 +1,12 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include +#include +#include #include #include +#include +#include #include #include #include @@ -14,9 +19,52 @@ double TraceMs() { return std::chrono::duration(std::chrono::steady_clock::now() - origin).count(); } -std::uint64_t SkippedShader() { - static const std::uint64_t skipped = [] { const char* text = std::getenv("APS5_SKIP_SHADER"); return text ? std::strtoull(text, nullptr, 16) : 0ull; }(); - return skipped; +namespace { + +constexpr std::size_t MaxSkippedShaders = 256; +std::array, MaxSkippedShaders> skippedShaders{}; +std::atomic skippedShaderCount{0}; + +void ReloadSkippedShaders(const char* path) { + static std::mutex reload; + static std::filesystem::file_time_type seen{}; + const std::unique_lock lock(reload, std::try_to_lock); + if (!lock.owns_lock()) return; + std::error_code error; + const auto stamp = std::filesystem::last_write_time(path, error); + if (error || stamp == seen) return; + seen = stamp; + std::ifstream file(path); + std::size_t count = 0; + skippedShaderCount.store(0, std::memory_order_release); + for (std::string word; count < MaxSkippedShaders && file >> word;) { + if (word.starts_with('#')) { + std::getline(file, word); + continue; + } + if (const auto address = std::strtoull(word.c_str(), nullptr, 16); address != 0) skippedShaders[count++].store(address, std::memory_order_relaxed); + } + skippedShaderCount.store(count, std::memory_order_release); + std::fprintf(stderr, "[skip-shader] %zu programs listed in %s\n", count, path); +} + +} + +bool SkippedShader(std::uint64_t address) { + static const std::uint64_t single = [] { const char* text = std::getenv("APS5_SKIP_SHADER"); return text ? std::strtoull(text, nullptr, 16) : 0ull; }(); + static const char* const file = std::getenv("APS5_SKIP_SHADER_FILE"); + if (address == 0) return false; + if (address == single) return true; + if (file == nullptr) return false; + static std::atomic nextCheck{0}; + const auto now = std::chrono::steady_clock::now().time_since_epoch(); + auto due = nextCheck.load(std::memory_order_relaxed); + if (now.count() >= due && nextCheck.compare_exchange_strong(due, std::chrono::duration_cast(now + std::chrono::milliseconds(500)).count(), std::memory_order_relaxed)) ReloadSkippedShaders(file); + const auto count = skippedShaderCount.load(std::memory_order_acquire); + for (std::size_t index = 0; index < count; ++index) { + if (skippedShaders[index].load(std::memory_order_relaxed) == address) return true; + } + return false; } bool SkipRefusedDraws() { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp index 8110338e83..7bf173c6d7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -66,7 +66,7 @@ bool Driver::pipelineDispatch() { void Driver::dispatch(QueueState& queue, std::span packet, const Submission& submission, std::uint64_t indirectArguments) { PerformanceTimer timing("Driver.Dispatch"); const auto address = (static_cast(readRegister(queue.shader, 0x20c)) << 8u) | (static_cast(readRegister(queue.shader, 0x20d) & 0xffu) << 40u); - if (address == SkippedShader() && address != 0) return; + if (SkippedShader(address)) return; auto registeredShader = RegisteredProgram(*submission.shaders, address, {0}); if (!registeredShader) registeredShader = ReadRawShader(address); const auto& snapshot = *registeredShader; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index 47cac201b9..57d3f93261 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -403,7 +403,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe for (const auto& patch : userPatches) writablePrograms[patch.program].userData[patch.index] = patch.value; tail.programs = writesPrograms ? &writablePrograms : &decode->programs; const auto& programs = *tail.programs; - if (const auto skipped = SkippedShader(); skipped != 0 && std::any_of(programs.begin(), programs.end(), [skipped](const DrawProgram& program) { return program.binary.codeAddress == skipped; })) return drawn(); + if (std::any_of(programs.begin(), programs.end(), [](const DrawProgram& program) { return SkippedShader(program.binary.codeAddress); })) return drawn(); const bool emptyIndirectSkip = EmptyIndirectSkip(); auto& cpuIndirectPrograms = CpuIndirectPrograms(); const auto recordDraws = [&](const Pm4::DrawArguments& arguments) { return arguments.count != 0 && arguments.instances != 0 && (!drawParameters.indexed || arguments.firstVertexOrIndex < drawParameters.indexCount); }; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SkipShaderFile.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SkipShaderFile.cpp new file mode 100644 index 0000000000..f3ea226714 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SkipShaderFile.cpp @@ -0,0 +1,100 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +alignas(256) constexpr std::array First{0xd7460000u, 0x04010c04u, 0x7e0202ffu, 0x12345678u, 0xe0702000u, 0x80000100u, 0xbf810000u}; +alignas(256) constexpr std::array Second{0xd7460000u, 0x04010c04u, 0x7e0202ffu, 0x12345678u, 0xe0702000u, 0x80000100u, 0xbf810000u}; +constexpr std::uint32_t Untouched = 0xcafebabeu; +constexpr std::uint32_t Stored = 0x12345678u; +alignas(256) std::array FirstOutput{}; +alignas(256) std::array SecondOutput{}; + +void Dispatch(const std::array& code, std::array& output) { + const auto address = reinterpret_cast(output.data()); + const std::array data{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), static_cast(output.size()), 0x00014204u}; + const auto program = reinterpret_cast(code.data()); + std::vector words{ + 0xc0037600u, 0x207, 64, 1, 1, + 0xc0027600u, 0x20c, static_cast(program >> 8u), static_cast(program >> 40u), + 0xc0017600u, 0x213, (4u << 1u) | (1u << 7u), + 0xc0047600u, 0x240, data[0], data[1], data[2], data[3], + 0xc0031500u, 1, 1, 1, 0x41u, + }; + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(address, sizeof(output)); +} + +std::string Hex(const void* address) { + char text[32]; + std::snprintf(text, sizeof(text), "%llx", static_cast(reinterpret_cast(address))); + return text; +} + +void List(const std::filesystem::path& path, const std::string& contents) { + std::ofstream(path, std::ios::trunc) << contents; + std::this_thread::sleep_for(std::chrono::milliseconds(1100)); +} + +void Run(const char* phase, bool firstSkipped, bool secondSkipped) { + FirstOutput.fill(Untouched); + SecondOutput.fill(Untouched); + Dispatch(First, FirstOutput); + Dispatch(Second, SecondOutput); + for (std::size_t i = 0; i < FirstOutput.size(); ++i) { + Require(FirstOutput[i] == (firstSkipped ? Untouched : Stored), std::string(phase) + ": the first shader's dispatch " + (firstSkipped ? "stored" : "did not store") + " dword " + std::to_string(i)); + Require(SecondOutput[i] == (secondSkipped ? Untouched : Stored), std::string(phase) + ": the second shader's dispatch " + (secondSkipped ? "stored" : "did not store") + " dword " + std::to_string(i)); + } +} + +} + +int main() { + const auto path = std::filesystem::temp_directory_path() / ("aps5-skip-shader-file-" + Hex(&FirstOutput) + ".txt"); + try { + List(path, "# listed\n" + Hex(First.data()) + "\n"); +#ifdef _WIN32 + Require(_putenv_s("APS5_SKIP_SHADER_FILE", path.string().c_str()) == 0, "cannot set APS5_SKIP_SHADER_FILE"); +#else + Require(setenv("APS5_SKIP_SHADER_FILE", path.string().c_str(), 1) == 0, "cannot set APS5_SKIP_SHADER_FILE"); +#endif + { + const auto device = OpenVulkanTestDevice(); + if (!device) { + std::filesystem::remove(path); + return VulkanTestSkipped; + } + } + Run("first listed", true, false); + List(path, Hex(Second.data()) + " # the first is not listed any more\n"); + Run("second listed", false, true); + List(path, ""); + Run("none listed", false, false); + AgcDriverShutdown_nid_postfix(); + std::filesystem::remove(path); + std::puts("skip shader file tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + std::error_code ignored; + std::filesystem::remove(path, ignored); + return 1; + } +} diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 93ed160653..68dbd80a37 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -378,7 +378,8 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_SKIP_INVALID_INDIRECT_REGISTERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | skips the entries whose offset dword is no register offset in a SET_*_REG_INDIRECT list read again where its packet executes, and writes its valid entries, instead of stopping | | `APS5_SKIP_REFUSED_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp` | | skips a draw whose graphics stage the recompiler refuses, reporting each refused shader once, instead of failing the queue | | `APS5_SKIP_REQUEST_DUMPS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp` | 8 | caps RecompileRequest dumps per shader-skip reason | -| `APS5_SKIP_SHADER` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp` | 0 | hex program address of a shader to skip preparing | +| `APS5_SKIP_SHADER` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp` | 0 | hex program address of a shader whose draws and dispatches are dropped | +| `APS5_SKIP_SHADER_FILE` | path | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp` | | file of whitespace-separated hex program addresses (`#` starts a comment) whose draws and dispatches are dropped; re-read within 500 ms of a change | | `APS5_SNAPSHOT_CHECK_FATAL` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | throws on the first snapshot that differs from registered memory instead of only counting them | | `APS5_SPANNED_WAVE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | spans a single-wave wave64 compute program over two full 32-lane subgroups | | `APS5_SPIRV_OPT` | string | `core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp` | | selects the SPIR-V optimization pipeline: `full` restores the performance pipeline, `none` skips optimization | From e75a166184bed8de6200e10d4a7d0a299d72f7c6 Mon Sep 17 00:00:00 2001 From: Shahul Mubeen A <174251444+shahulmubeen-a@users.noreply.github.com> Date: Thu, 22 Oct 2026 09:42:44 +0530 Subject: [PATCH 234/428] fix(agc): ignore skip-file tokens that are not whole hexadecimal addresses strtoull took the valid hex prefix of a token and ignored the rest, so a malformed token such as "123garbage" selected program 0x123, and an overflowing token was not detected. A token now has to be a whole 64-bit hex number (with an optional 0x prefix); any other token is reported and ignored. The test adds a list with a malformed token whose prefix is the first program's address and an overflowing token. Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/Driver/Diagnostics.cpp | 18 +++++++++++++++++- .../tests/execution/SkipShaderFile.cpp | 2 ++ docs/dev/ENVIRONMENT.md | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp index 56cf447c34..ca92adc259 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp @@ -2,15 +2,18 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include #include +#include #include #include #include #include #include #include +#include #include #include #include +#include namespace AgcDriver::DriverDetail { @@ -25,6 +28,14 @@ constexpr std::size_t MaxSkippedShaders = 256; std::array, MaxSkippedShaders> skippedShaders{}; std::atomic skippedShaderCount{0}; +std::optional programAddress(std::string_view word) { + if (word.starts_with("0x") || word.starts_with("0X")) word.remove_prefix(2); + std::uint64_t address = 0; + const auto [end, error] = std::from_chars(word.data(), word.data() + word.size(), address, 16); + if (word.empty() || error != std::errc{} || end != word.data() + word.size()) return std::nullopt; + return address; +} + void ReloadSkippedShaders(const char* path) { static std::mutex reload; static std::filesystem::file_time_type seen{}; @@ -42,7 +53,12 @@ void ReloadSkippedShaders(const char* path) { std::getline(file, word); continue; } - if (const auto address = std::strtoull(word.c_str(), nullptr, 16); address != 0) skippedShaders[count++].store(address, std::memory_order_relaxed); + const auto address = programAddress(word); + if (!address) { + std::fprintf(stderr, "[skip-shader] ignoring '%s' in %s: not a hexadecimal program address\n", word.c_str(), path); + continue; + } + if (*address != 0) skippedShaders[count++].store(*address, std::memory_order_relaxed); } skippedShaderCount.store(count, std::memory_order_release); std::fprintf(stderr, "[skip-shader] %zu programs listed in %s\n", count, path); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SkipShaderFile.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SkipShaderFile.cpp index f3ea226714..cec7c1cc4c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SkipShaderFile.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SkipShaderFile.cpp @@ -84,6 +84,8 @@ int main() { Run("first listed", true, false); List(path, Hex(Second.data()) + " # the first is not listed any more\n"); Run("second listed", false, true); + List(path, Hex(First.data()) + "zz 1" + std::string(16, '0') + " 0x" + Hex(Second.data()) + "\n"); + Run("malformed and overflowing tokens", false, true); List(path, ""); Run("none listed", false, false); AgcDriverShutdown_nid_postfix(); diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 68dbd80a37..f7b60194cb 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -379,7 +379,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_SKIP_REFUSED_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp` | | skips a draw whose graphics stage the recompiler refuses, reporting each refused shader once, instead of failing the queue | | `APS5_SKIP_REQUEST_DUMPS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp` | 8 | caps RecompileRequest dumps per shader-skip reason | | `APS5_SKIP_SHADER` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp` | 0 | hex program address of a shader whose draws and dispatches are dropped | -| `APS5_SKIP_SHADER_FILE` | path | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp` | | file of whitespace-separated hex program addresses (`#` starts a comment) whose draws and dispatches are dropped; re-read within 500 ms of a change | +| `APS5_SKIP_SHADER_FILE` | path | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp` | | file of whitespace-separated hex program addresses (`#` starts a comment) whose draws and dispatches are dropped; re-read within 500 ms of a change; a token that is not a whole 64-bit hex number is reported and ignored | | `APS5_SNAPSHOT_CHECK_FATAL` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | throws on the first snapshot that differs from registered memory instead of only counting them | | `APS5_SPANNED_WAVE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | spans a single-wave wave64 compute program over two full 32-lane subgroups | | `APS5_SPIRV_OPT` | string | `core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp` | | selects the SPIR-V optimization pipeline: `full` restores the performance pipeline, `none` skips optimization | From a3e2626eea63c29887a4055ad60aea0287183307 Mon Sep 17 00:00:00 2001 From: Bram910 Date: Sat, 24 Oct 2026 20:16:43 +0200 Subject: [PATCH 235/428] test(tools): avoid private subprocess encoding helper --- tools/tests/test_suggest_patches.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/tests/test_suggest_patches.py b/tools/tests/test_suggest_patches.py index 0d892bb7f9..e6402342a8 100644 --- a/tools/tests/test_suggest_patches.py +++ b/tools/tests/test_suggest_patches.py @@ -73,12 +73,12 @@ class PatchAdvisorTests(unittest.TestCase): def verify(command, **options): script = f"import os; os.write(1, {stdout!r}); os.write(2, {stderr!r})" + options.setdefault("encoding", "ascii") result = actual_run([sys.executable, "-c", script], **options) captured.append(result) return result - with patch.object(suggest_patches.subprocess, "_text_encoding", return_value="ascii"), \ - patch.object(suggest_patches.subprocess, "run", side_effect=verify): + with patch.object(suggest_patches.subprocess, "run", side_effect=verify): self.assertEqual(self.advise(), 0) self.assertEqual(captured[0].stdout, stdout.decode("utf-8", errors="replace")) self.assertEqual(captured[0].stderr, stderr.decode("utf-8", errors="replace")) From 7424949b8bb2dd04011a683dbc1949b8ee6cd7de Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Sat, 24 Oct 2026 19:47:43 +0200 Subject: [PATCH 236/428] fix(relinker): refuse branches into trapped guest GS instructions on Windows Windows output replaced each guest GS memory instruction with UD2 and NOP fill without checking the decoded direct branch targets, so a branch past the first byte of such an instruction was converted and landed in the second UD2 byte or the NOP fill. A branch target strictly inside a trapped GS instruction is now refused with "Branch enters a guest GS instruction", the rule macOS applies since #7706 and this builder already applies to its FS rewrites. The branch targets are sorted before the trap loop, which runs before the PT_TLS early returns, so images without PT_TLS are covered; guest modules go through the same builder. A branch to the first byte still converts and reaches the UD2. --- .../elfpatcher/src/windows/WindowsTlsBuilder.cpp | 11 +++++++---- .../relinker/tests/test_tls_function_coverage.py | 2 ++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index b78a010b40..f923bafd63 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -221,8 +221,14 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co } report(9000); - for (const auto& [rva, length] : gsTraps) + std::sort(branchTargets.begin(), branchTargets.end()); + branchTargets.erase(std::unique(branchTargets.begin(), branchTargets.end()), branchTargets.end()); + for (const auto& [rva, length] : gsTraps) { + const auto target = std::upper_bound(branchTargets.begin(), branchTargets.end(), rva); + if (target != branchTargets.end() && *target < rva + length) + throw Domain::RelinkerException("Branch enters a guest GS instruction", *target); trapInstruction(sections, rva, length); + } if (tls == nullptr) { if (!accesses.empty()) throw Domain::RelinkerException("Guest TLS access without PT_TLS"); @@ -274,9 +280,6 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co code.PatchBranch(skipCallback, code.GetRva()); code.Emit({0xc3}); - std::sort(branchTargets.begin(), branchTargets.end()); - branchTargets.erase(std::unique(branchTargets.begin(), branchTargets.end()), branchTargets.end()); - std::uint64_t patched = 0; for (const auto& access : accesses) { if (patched % 256 == 0) report(9000 + 900 * patched / accesses.size()); diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py index 4687c088bf..8c78d020bd 100644 --- a/core/relinker/relinker/tests/test_tls_function_coverage.py +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -777,6 +777,8 @@ def main(): conflicting[232:344] = unwind[232:344] struct.pack_into(" Date: Sat, 24 Oct 2026 19:53:08 +0200 Subject: [PATCH 237/428] fix(relinker): reject executable DT_NEEDED names the guest reader rejects The pipeline read an executable's DT_NEEDED names with readCStr and checked only bounds and the NUL terminator, so without bundled modules an empty name, $LIB.prx, a:b.prx or a name with CR or LF relinked with exit 0 and reached the Linux DT_NEEDED unchanged, while the Windows and macOS outputs each rejected a different subset. The DT_NEEDED loop now applies the rule GuestImageReader applies to a module's DT_NEEDED: a name is not empty and contains no backslash, colon, dollar, CR or LF; a slash stays allowed. Such an executable now fails on every target with "Invalid dependency: " at the name's file offset, and outputs for valid names do not change. --- .../src/pipeline/RelinkerPipeline.cpp | 2 + .../relinker/tests/test_needed_libraries.py | 52 +++++++++++++++++-- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 77044446d2..720c52fdee 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -277,6 +277,8 @@ RelinkResult RelinkerPipeline::Relink() { for (auto& [fst, snd] : neededLibraryNamesByStrOffset) { snd = readCStr(fst); + if (snd.empty() || snd.find_first_of("\\:$\r\n") != std::string::npos) + throw RelinkerException("Invalid dependency: " + snd, dynStrTabOffset + fst); neededLibraries.push_back(snd); } diff --git a/core/relinker/relinker/tests/test_needed_libraries.py b/core/relinker/relinker/tests/test_needed_libraries.py index 9bf39efb8f..06a4ddb1d5 100644 --- a/core/relinker/relinker/tests/test_needed_libraries.py +++ b/core/relinker/relinker/tests/test_needed_libraries.py @@ -6,15 +6,15 @@ import sys import tempfile from test_guest_intel_trampolines import PLAIN_SITE, guest_fixture -from test_guest_symbol_names import importing_executable +from test_guest_symbol_names import DYNAMIC, RELOCATIONS, STRINGS, SYMBOLS, importing_executable sys.path.insert(0, str(Path(__file__).resolve().parents[4] / "tools")) import import_audit -def named_module(tag): +def named_module(tag, name="Party.prx"): image = guest_fixture(PLAIN_SITE) - strings = b"\0Party.prx\0" + strings = b"\0" + name.encode() + b"\0" image[0x800:0x800 + len(strings)] = strings struct.pack_into(" Date: Sat, 24 Oct 2026 11:02:01 -0700 Subject: [PATCH 238/428] test(shader): pin SDWA v_cvt_f32_ubyte source selects measured on RDNA2 v_cvt_f32_ubyte0 to v_cvt_f32_ubyte3 take byte N of source 0 after SRC0_SEL and SRC0_SEXT select and extend it, as measured with the hardware oracle on RDNA2 (gfx1036) for all seven source selects with and without SRC0_SEXT. The test checked four combinations derived from the ISA guide; it now checks every combination against the measured results, and the debt entry keeps only what is not measured: gfx1013. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/SdwaCvtF32Ubyte.cpp | 67 ++++++++++++++----- docs/dev/debt/shader.md | 2 +- 2 files changed, 52 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SdwaCvtF32Ubyte.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SdwaCvtF32Ubyte.cpp index 4468d5d220..dac88be437 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SdwaCvtF32Ubyte.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SdwaCvtF32Ubyte.cpp @@ -17,20 +17,50 @@ using ShaderRecompiler::ShaderStage; constexpr std::uint32_t Threads = 64; constexpr std::uint32_t Inputs = 4; constexpr std::uint32_t Results = 4; +constexpr std::uint32_t Selectors = 7; alignas(256) std::array Input{}; alignas(256) std::array Output{}; -alignas(256) constexpr std::array SdwaCode{ +constexpr std::array ModifierWords{14, 16, 18, 20}; + +alignas(256) std::array SdwaCode{ 0x34020082, 0x34060082, 0xe0302000, 0x80000401, 0x7e1402ff, 0xabcd1234, 0x7e1602ff, 0xabcd1234, 0x7e1802ff, 0xabcd1234, 0x7e1a02ff, 0xabcd1234, - 0xbf8c3f70, 0x7e1424f9, 0x00040604, 0x7e1624f9, 0x00050604, 0x7e1826f9, 0x000d0604, 0x7e1a28f9, 0x000b0604, + 0xbf8c3f70, 0x7e1422f9, 0x00060604, 0x7e1624f9, 0x00060604, 0x7e1826f9, 0x00060604, 0x7e1a28f9, 0x00060604, 0xe0702000, 0x80010a03, 0xe0702004, 0x80010b03, 0xe0702008, 0x80010c03, 0xe070200c, 0x80010d03, 0xbf810000, }; +const std::array SelectorNames{"BYTE_0", "BYTE_1", "BYTE_2", "BYTE_3", "WORD_0", "WORD_1", "DWORD"}; + +struct SelectVector { + std::uint32_t value; + std::array zeroExtended; + std::array signExtended; +}; + +constexpr std::array Vectors{{ + {0x00000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0xffffffffu, {0x000000ffu, 0x000000ffu, 0x000000ffu, 0x000000ffu, 0x0000ffffu, 0x0000ffffu, 0xffffffffu}, {0xffffffffu, 0xffffffffu, 0xffffffffu, 0xffffffffu, 0xffffffffu, 0xffffffffu, 0xffffffffu}}, + {0x12345678u, {0x00000078u, 0x00000056u, 0x00000034u, 0x00000012u, 0x00005678u, 0x00001234u, 0x12345678u}, {0x00000078u, 0x00000056u, 0x00000034u, 0x00000012u, 0x00005678u, 0x00001234u, 0x12345678u}}, + {0x80808080u, {0x00000080u, 0x00000080u, 0x00000080u, 0x00000080u, 0x00008080u, 0x00008080u, 0x80808080u}, {0xffffff80u, 0xffffff80u, 0xffffff80u, 0xffffff80u, 0xffff8080u, 0xffff8080u, 0x80808080u}}, + {0x7f7f7f7fu, {0x0000007fu, 0x0000007fu, 0x0000007fu, 0x0000007fu, 0x00007f7fu, 0x00007f7fu, 0x7f7f7f7fu}, {0x0000007fu, 0x0000007fu, 0x0000007fu, 0x0000007fu, 0x00007f7fu, 0x00007f7fu, 0x7f7f7f7fu}}, + {0x8182a3f4u, {0x000000f4u, 0x000000a3u, 0x00000082u, 0x00000081u, 0x0000a3f4u, 0x00008182u, 0x8182a3f4u}, {0xfffffff4u, 0xffffffa3u, 0xffffff82u, 0xffffff81u, 0xffffa3f4u, 0xffff8182u, 0x8182a3f4u}}, + {0x7f00ff80u, {0x00000080u, 0x000000ffu, 0x00000000u, 0x0000007fu, 0x0000ff80u, 0x00007f00u, 0x7f00ff80u}, {0xffffff80u, 0xffffffffu, 0x00000000u, 0x0000007fu, 0xffffff80u, 0x00007f00u, 0x7f00ff80u}}, + {0x00008000u, {0x00000000u, 0x00000080u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00000000u, 0x00008000u}, {0x00000000u, 0xffffff80u, 0x00000000u, 0x00000000u, 0xffff8000u, 0x00000000u, 0x00008000u}}, + {0x0000007fu, {0x0000007fu, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000007fu, 0x00000000u, 0x0000007fu}, {0x0000007fu, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000007fu, 0x00000000u, 0x0000007fu}}, + {0x00000080u, {0x00000080u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000080u, 0x00000000u, 0x00000080u}, {0xffffff80u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000080u, 0x00000000u, 0x00000080u}}, + {0x80000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000080u, 0x00000000u, 0x00008000u, 0x80000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0xffffff80u, 0x00000000u, 0xffff8000u, 0x80000000u}}, + {0x00800000u, {0x00000000u, 0x00000000u, 0x00000080u, 0x00000000u, 0x00000000u, 0x00000080u, 0x00800000u}, {0x00000000u, 0x00000000u, 0xffffff80u, 0x00000000u, 0x00000000u, 0x00000080u, 0x00800000u}}, + {0x0000ff00u, {0x00000000u, 0x000000ffu, 0x00000000u, 0x00000000u, 0x0000ff00u, 0x00000000u, 0x0000ff00u}, {0x00000000u, 0xffffffffu, 0x00000000u, 0x00000000u, 0xffffff00u, 0x00000000u, 0x0000ff00u}}, + {0xdeadbeefu, {0x000000efu, 0x000000beu, 0x000000adu, 0x000000deu, 0x0000beefu, 0x0000deadu, 0xdeadbeefu}, {0xffffffefu, 0xffffffbeu, 0xffffffadu, 0xffffffdeu, 0xffffbeefu, 0xffffdeadu, 0xdeadbeefu}}, + {0x01020304u, {0x00000004u, 0x00000003u, 0x00000002u, 0x00000001u, 0x00000304u, 0x00000102u, 0x01020304u}, {0x00000004u, 0x00000003u, 0x00000002u, 0x00000001u, 0x00000304u, 0x00000102u, 0x01020304u}}, + {0xf0e0d0c0u, {0x000000c0u, 0x000000d0u, 0x000000e0u, 0x000000f0u, 0x0000d0c0u, 0x0000f0e0u, 0xf0e0d0c0u}, {0xffffffc0u, 0xffffffd0u, 0xffffffe0u, 0xfffffff0u, 0xffffd0c0u, 0xfffff0e0u, 0xf0e0d0c0u}}, +}}; + void FillInput() { for (std::uint32_t tid = 0; tid < Threads; ++tid) { - Input[tid * Inputs] = tid | ((tid + 64u) << 8u) | ((255u - tid) << 16u) | (((tid * 3u + 128u) & 0xffu) << 24u); + Input[tid * Inputs] = Vectors[tid % Vectors.size()].value; } } @@ -49,7 +79,10 @@ std::array BufferDescriptor(const void* data, std::uint32_t co return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), count, 0x11016facu}; } -void Run(AgcDriver::VulkanDevice& device) { +void Run(AgcDriver::VulkanDevice& device, std::uint32_t selector, bool signExtend) { + for (const auto word : ModifierWords) { + SdwaCode[word] = 0x00000604u | (selector << 16u) | (signExtend ? 1u << 19u : 0u); + } Output.fill(0xdeadbeefu); std::vector userData(8, 0u); const auto input = BufferDescriptor(Input.data(), static_cast(Input.size())); @@ -71,18 +104,16 @@ void Run(AgcDriver::VulkanDevice& device) { device.WaitIdle(); } -void Check() { +void Check(std::uint32_t selector, bool signExtend) { for (std::uint32_t tid = 0; tid < Threads; ++tid) { - const auto source = Input[tid * Inputs]; + const auto& vector = Vectors[tid % Vectors.size()]; + const auto selected = signExtend ? vector.signExtended[selector] : vector.zeroExtended[selector]; const auto* out = &Output[tid * Results]; - const auto where = [&](std::uint32_t result) { return "sdwa cvt f32 ubyte: thread " + std::to_string(tid) + " result " + std::to_string(result) + " is " + Hex(out[result]); }; - const auto byte1 = FloatBits((source >> 8u) & 0xffu); - const auto byte3 = FloatBits(source >> 24u); - const auto signFill = FloatBits((source & 0x80000000u) != 0u ? 0xffu : 0u); - Require(out[0] == byte1, where(0) + ", expected " + Hex(byte1) + ": v_cvt_f32_ubyte1 src0_sel:WORD_0 must convert bits 15:8"); - Require(out[1] == byte3, where(1) + ", expected " + Hex(byte3) + ": v_cvt_f32_ubyte1 src0_sel:WORD_1 must convert bits 31:24"); - Require(out[2] == signFill, where(2) + ", expected " + Hex(signFill) + ": v_cvt_f32_ubyte2 src0_sel:WORD_1 src0_sext must convert the sign extension of the high word"); - Require(out[3] == signFill, where(3) + ", expected " + Hex(signFill) + ": v_cvt_f32_ubyte3 src0_sel:BYTE_3 src0_sext must convert the sign extension of the top byte"); + for (std::uint32_t byte = 0; byte < Results; ++byte) { + const auto expected = FloatBits((selected >> (byte * 8u)) & 0xffu); + Require(out[byte] == expected, "sdwa v_cvt_f32_ubyte" + std::to_string(byte) + " src0_sel:" + SelectorNames[selector] + (signExtend ? " src0_sext" : "") + " of " + + Hex(vector.value) + " is " + Hex(out[byte]) + ", expected " + Hex(expected)); + } } } @@ -93,8 +124,12 @@ int main() { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; FillInput(); - Run(*device); - Check(); + for (const bool signExtend : {false, true}) { + for (std::uint32_t selector = 0; selector < Selectors; ++selector) { + Run(*device, selector, signExtend); + Check(selector, signExtend); + } + } std::puts("sdwa cvt f32 ubyte tests passed"); return 0; } catch (const std::exception& error) { diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index db3130a1f6..7f7117bce5 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -188,7 +188,7 @@ - [BDA anchor halfword reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) retain volatile byte loads even for noncoherent memory. On RX 6650 XT / RADV Mesa 26.2.4, the valid SPIR-V for two adjacent nonvolatile byte loads returned `0x80a0` for bytes `0xa0, 0xa1`, where lookup-based reads returned `0xa1a0`. Adding the volatile operand makes the same GPU exactness fixture pass, including mixed direct/fallback lanes. The driver transformation responsible has not been isolated; removing this restriction needs equivalent readback coverage on the affected driver. Single-byte reads keep their original access mask. - [BDA span reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) (shader recompiler) - `read_bda_span` is one selection: one arm calls `load_bda_span`, a `DontInline` function that holds the in-place dword loads and calls nothing, and the other arm is the per-dword byte fallback. Two other shapes of the same reads each hang a group of NVIDIA drivers, where a dispatch never finishes and the device is lost. With the loads emitted in the arm itself, a dispatch whose lanes took both arms hung on an RTX 3060 and an RTX 4060 Laptop GPU (Windows, driver 591.86) and an RTX 5060 (Linux, driver 590.48.01) ([#3291](https://github.com/boykopovar/AnyPS5/issues/3291)). With the loads and the fallback as two selections, the first under the span's condition and the second under its negation, it hung on an RTX 4070 Laptop GPU (Windows, drivers 595.97 and 596.36), an RTX 5080 (Windows, driver 610.62) and an RTX 4090 (Linux, driver 610.57.04) ([#4975](https://github.com/boykopovar/AnyPS5/issues/4975)). The current shape is measured on the RTX 4090 with driver 610.57.04 only, where it does not hang with or without SPIRV-Tools; a shape with the loads in their own `DontInline` function was reported passing on driver 591.86 in #3291. The cause inside the driver is not known. - [GFX10 scratch address operands](../../../core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp) (shader recompiler) - `core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp` and `core/shader/recompiler/Translation/src/MemoryInstructions/MemoryResourceHelpers.cpp` use the SV/SS/ST scratch operand widths described by LLVM FLATInstructions.td at 3eb867f9b63ae2881bbe3c25d3c12842f2bdc55e (operand pseudos L557-L630, GFX10 encoding L2973-L3038). Decoding and IR tests cover the one-dword VGPR offset; PS5 hardware address-result validation remains outstanding. Unsupported scalar registers still throw. -- [SDWA source selects on `v_cvt_f32_ubyte1`, `v_cvt_f32_ubyte2` and `v_cvt_f32_ubyte3`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (shader recompiler) - byte N is taken from source 0 after `SRC0_SEL` and `SRC0_SEXT` select and extend it, since SDWA supplies source 0 (RDNA 2 ISA guide 6.8 and 13.3.7) and the opcode reads `S0.u[8N+7:8N]` (12.8). Not measured on hardware. +- [SDWA source selects on `v_cvt_f32_ubyte0` to `v_cvt_f32_ubyte3`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (shader recompiler) - the PS5's gfx1013 is not measured. Byte N is taken from source 0 after `SRC0_SEL` and `SRC0_SEXT` select and extend it, as measured on RDNA2 (gfx1036, Ryzen 7 9800X3D integrated graphics, AMD Windows driver 32.0.21030.31) for all seven source selects with and without `SRC0_SEXT` over 16 sources (zero, all ones, each byte's and word's sign bit set alone, mixed patterns), wave32, IEEE 0, DX10_CLAMP 1, f32 denormals flushed, f16 denormals kept, round to nearest even, FP16_OVFL 0. - [Flag structurization](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a graph that every structurizer attempt refuses, irreducible and non-terminating graphs included, is rebuilt with one reach flag per block and one again flag per loop (`structurizeWithFlags`): each block keeps one copy and runs behind a guard on its flag, and each strongly connected component becomes a loop whose single latch repeats it while a back edge set its flag. The output has about 8 times the input's blocks and one route variable per block, and ends in an empty Return that never runs. Only blocks that start the top-level order unconditionally run unguarded, so resource planning also sees the static paths that skip a guarded block and can plan extra descriptor candidates for them. The attempts still run first. Shared merge splitting refuses an attempt once the graph has grown past 16 times its block count at the start of splitting (at least 64 blocks); succeeding attempts grew at most 10.6 times on random graphs of up to 128 blocks, and an attempt that would succeed only beyond the budget leaves the graph to a later attempt or to this construction. Splitting that adds one block per round stays within that budget until its 4 rounds per block run out, so the attempts can still take more than 10 s (9 of 2,000 random graphs of up to 128 blocks, 63 s at most). `APS5_FORCE_STRUCTURIZER_FALLBACK=1` builds every graph this way, for tests - [Shared loop latches](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a loop whose single latch holds instructions and is reached by one edge gets an empty continue block, so its continue construct is that one block. A latch that several `continue` paths share keeps its instructions in the continue construct, since isolating it would clone them into each path. When such a latch emits more than one SPIR-V block, as a memory or image access does, SPIRV-Cross (MoltenVK) folds the construct into a `for` increment and drops the header's `OpPhi` updates, so the loop never advances on Metal. No title is known to reach this case. - [Scalar buffer descriptors](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) use only the base address, stride and record count for resources accessed exclusively by scalar loads; their fourth word is normalized to zero, following AMD RDNA2 ISA section 7.2.2. Vector and mixed accesses retain descriptor validation. Native Vulkan readbacks cover ignored fourth-word fields on Windows/NVIDIA; their console behavior has not been measured. From 041df5af171071cc6c8a58355aa44a485ad96b86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 24 Oct 2026 20:42:43 +0200 Subject: [PATCH 239/428] fix(agc): record an evicting import's range as a pair for the mirror release Co-Authored-By: Claude Sonnet 5.5 --- .../libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index ec6af8e0a3..e6144b3e8c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -4051,7 +4051,7 @@ void GuestBufferMemory::AcquireRegistered() { region.hostBacked = true; region.direct = entry; regions.push_back(std::move(region)); - imported.push_back(range->address); + imported.emplace_back(range->address, range->address + range->bytes); continue; } copiedBytes += committed; From 50dc1c93c8f3095d533ab7cc0b7f64cb0f8cec87 Mon Sep 17 00:00:00 2001 From: Filip Michalski Date: Sat, 24 Oct 2026 20:43:16 +0200 Subject: [PATCH 240/428] fix(relinker): reject LOCK-prefixed MOVNTSS and MOVNTSD --- .../codegen/src/x86/Amd64OnlyInstructionMatcher.cpp | 2 ++ core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp index 065e2ad80e..ecbe487147 100644 --- a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp +++ b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp @@ -201,6 +201,8 @@ private: Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchMovnts(const DecodedInstruction& instr, const Entry& entry) const { using namespace X64OpcodeConstants; const auto opcodeOffset = instr.OpcodeOffset(); + if (std::find(instr.Data, instr.Data + opcodeOffset, PrefixLock) != instr.Data + opcodeOffset) + throw CodegenException("MOVNTSS/MOVNTSD with a LOCK prefix"); if (opcodeOffset + 2 >= instr.Length) throw CodegenException("MOVNTSS/MOVNTSD truncated before its ModRM byte"); const auto modrm = instr.Data[opcodeOffset + 2]; diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index ca2cf46715..96f668abbf 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -322,6 +322,8 @@ void matcherSubstitutions() { const auto movntsd = match({0xF2, 0x44, 0x0F, 0x2B, 0x4C, 0x24, 0x10}); require(movntsd && movntsd->Lowering == Codegen::Amd64OnlyLowering::InPlace && movntsd->ReplacementBytes == Bytes{0xF2, 0x44, 0x0F, 0x11, 0x4C, 0x24, 0x10} && movntsd->InstructionName == "MOVNTSD", "MOVNTSD was not rewritten to MOVSD"); requireFailure([&] { (void)match({0xF3, 0x0F, 0x2B, 0xC1}); }, "MOVNTSS with a register operand was accepted"); + const auto movntssDisplacement = match({0xF3, 0x0F, 0x2B, 0x47, 0xF0}); + require(movntssDisplacement && movntssDisplacement->ReplacementBytes == Bytes{0xF3, 0x0F, 0x11, 0x47, 0xF0}, "MOVNTSS displacement byte was treated as a LOCK prefix"); const auto monitorx = match({0x0F, 0x01, 0xFA}); require(monitorx && monitorx->Lowering == Codegen::Amd64OnlyLowering::InPlace && monitorx->ReplacementBytes == Bytes{0x0F, 0x1F, 0x00} && monitorx->InstructionName == "MONITORX", "MONITORX was not replaced by a NOP"); const auto mwaitx = match({0x0F, 0x01, 0xFB}); @@ -506,6 +508,13 @@ void converterSegment() { lockedRdpru[0x211] = 0x01; lockedRdpru[0x212] = 0xFD; requireFailure([&] { (void)converter->Convert(lockedRdpru, {segmentHeader(20)}); }, "LOCK RDPRU was accepted"); + for (const Bytes& instruction : {Bytes{0xF0, 0xF3, 0x0F, 0x2B, 0x07}, Bytes{0xF3, 0xF0, 0x0F, 0x2B, 0x07}, + Bytes{0xF0, 0xF2, 0x0F, 0x2B, 0x07}, Bytes{0xF2, 0xF0, 0x0F, 0x2B, 0x07}}) { + auto lockedMovnts = file; + std::copy(instruction.begin(), instruction.end(), lockedMovnts.begin() + 0x20F); + lockedMovnts[0x214] = 0xC3; + require(failureOffset([&] { (void)converter->Convert(lockedMovnts, {segmentHeader(21)}); }, "LOCK MOVNTSS/MOVNTSD was accepted") == 0x20F, "LOCK MOVNTSS/MOVNTSD failure does not carry the file offset"); + } auto registerForm = file; const Bytes extrqRegister = {0x66, 0x0F, 0x79, 0xCA}; std::copy(extrqRegister.begin(), extrqRegister.end(), registerForm.begin() + 0x20F); From a89e4c35a3234b32c2ccdf8737e11ba24832bc06 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Thu, 22 Oct 2026 02:20:50 +0700 Subject: [PATCH 241/428] docs(libkernel): correct three stale socket statements Co-Authored-By: Claude --- docs/dev/debt/libkernel.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index ead756f295..6885a377f0 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -86,7 +86,7 @@ - [tcgetattr, tcsetattr](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) - every file and pipe descriptor is taken as not a terminal (`ENOTTY`), and a socket gets `EADDRNOTAVAIL` for `AF_INET` and `EOPNOTSUPP` otherwise, as FreeBSD's `in_control` and `pru_control_notsupp` answer a termios ioctl; what descriptors 0 to 2 of a title are on the console, and whether any of them is a terminal there, is unknown - [cfsetspeed](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel, NID `C1MQ6OaXRjo`) - console behavior and module/library versions are unverified. The prototype, uint32 speed and 44-byte termios layout (input speed at 36, output speed at 40) follow the public PS5 payload SDK [termios.h](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/termios.h) and [sys/_termios.h](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/sys/_termios.h). The module association follows its libkernel stub table and the NID rehashes to the aerolib name. Both speed fields accept every uint32 value and no flags or control characters change, as [FreeBSD 11.4 termios.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/gen/termios.c) does. No terminal I/O is performed. Null pointers throw where the reference dereferences them. - [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - descriptor 0 is never reported readable and descriptors 1 and 2 are always writable, as for a console title with no input; reads from descriptor 0 still go to the host stdin. `/dev/random` and `/dev/urandom` follow `randomdev_poll` for a seeded device under [`selscan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1178-L1236), which asks once with the flags of every set the descriptor is in: in the read set they are readable and reported in no other set, and otherwise they are reported in the write and exception sets they were asked in. A regular file, a directory or a file read from app0.zar is always readable and writable and never in the exception set, as `vop_stdpoll` answers, on every host. Guest sockets and libSceNet sockets are polled. A kqueue is readable while one of its events is triggered and is never writable or in the exception set, as FreeBSD 11.4's [`kqueue_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c#L1900-L1924) answers, on every host. Other descriptors are polled through the host on Linux, while on Windows pipes are checked every millisecond with the readiness test `poll` uses and any other descriptor throws. Dungeons (PPSA16064) selects on descriptor 0, and RetroArch (PPSA99169) selects on its web server's wake-up pipe next to its listening socket -- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - a descriptor the socket poller reports as unknown that is below `GuestSockets::FirstDescriptor` is polled on the host instead of throwing (on Windows only a pipe, as above); an invalid host descriptor fails with `EBADF`, and mixed host and socket sets are polled in short alternating slices so neither side waits out the full slice alone. A descriptor that stops being a socket, a kqueue or a pipe during the wait fails the call with `EBADF` when it is closed and is selected again with the rest of the timeout when its number was reused, as FreeBSD 11.4's [`kern_select`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1129-L1140) looks every descriptor up again in each pass. A close that has already removed the descriptor from the guest's kqueue, pipe or file tables but not yet closed its host descriptor is seen by that new look-up as a descriptor of unknown kind, which throws on Windows and is polled through the host on Linux. Wobbly Life (PPSA29413) selects on file descriptors while streaming its bundles +- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - a descriptor the socket poller reports as unknown is polled on the host instead of throwing (on Windows only a pipe, as above); an invalid host descriptor fails with `EBADF`, and mixed host and socket sets are polled in short alternating slices so neither side waits out the full slice alone. A descriptor that stops being a socket, a kqueue or a pipe during the wait fails the call with `EBADF` when it is closed and is selected again with the rest of the timeout when its number was reused, as FreeBSD 11.4's [`kern_select`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1129-L1140) looks every descriptor up again in each pass. A close that has already removed the descriptor from the guest's kqueue, pipe or file tables but not yet closed its host descriptor is seen by that new look-up as a descriptor of unknown kind, which throws on Windows and is polled through the host on Linux. Wobbly Life (PPSA29413) selects on file descriptors while streaming its bundles - [__progname](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export - [environ](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - data export pointing to a fixed empty environment so titles that import it load; it is not the environment libc's `getenv` reads, so it lists none of those variables and `setenv`/`putenv` changes do not appear in it - [seteuid, setegid, setuid, setgid, setreuid, setregid, setresuid, setresgid, setgroups, getgroups](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the guest process is assumed to run as root, with real, effective and saved user and group ids 0. As in FreeBSD's `sys_seteuid`, `sys_setegid`, `sys_setuid`, `sys_setgid`, `sys_setreuid`, `sys_setregid`, `sys_setresuid` and `sys_setresgid`, id 0 (or -1 for an unchanged `setreuid`/`setregid`/`setresuid`/`setresgid` id) is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change an id, which is not modelled, and throws. `setgroups` keeps the supplementary group list as `kern_setgroups` does (`NGROUPS_MAX` 1023, an empty list leaves the effective group as the only entry, a first entry other than 0 would change the effective group and throws) and `getgroups` reports it. The ids and privileges a title has on a console are unverified @@ -160,7 +160,7 @@ - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a read event on a listening socket, here and in `kevent`, reports the number of completed connections waiting to be accepted in `data`, as FreeBSD 11.4's [`filt_solisten`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3384-L3392) reports `so_qlen`: on Linux a TCP listener's accept queue from `TCP_INFO` (`tcpi_unacked`, which Linux fills with the accept backlog for a listening socket), and elsewhere, or for a listener that is not TCP, 1 while a connection is waiting, as no host call reports that count. A write event reports the free space of the socket's send buffer in `data`, as FreeBSD 11.4's [`filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3358-L3382) reports `sbspace`: on Linux the host's `SO_SNDBUF` less the bytes `SIOCOUTQ` counts as queued, which include the host's per-packet overhead, so the space shrinks by somewhat more than the bytes sent. Windows has no query for that space and macOS is not wired up, so both report the whole `SO_SNDBUF`, which does not shrink as data is queued. Only guest sockets are supported; other descriptors and a low watermark above 1 throw - [sceKernelAddReadEvent on pipes](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) is an exception to the sockets-only rule above: it also takes the read end of a pipe, as `kevent` does through FreeBSD's `filt_piperead`, and ignores the size argument there. Host streams, other descriptors and a write event on a pipe throw - [sceKernelAddReadEvent and sceKernelAddWriteEvent on regular files](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) are the other exception: a size argument above 1, directories and other vnode types throw, and whether the console's file systems answer as `vfs_kqfilter` does was not measured. A wait holding one of these events polls it every millisecond, as no host call reports every file change, so a write through another descriptor shows up to 1 ms late. Unlike FreeBSD, a seek can make a read event fire again: FreeBSD 11.4's [`kqueue_scan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c) deactivates a level-triggered read knote once `filt_vfsread` reports `data` 0, and only a vnode `KNOTE` (a write, an extend or a truncate) activates it again; `lseek` posts none, so after a file read to its end is seeked back, the 1 ms poll here reports the new `data` (10 for a 10-byte file seeked to 0) where FreeBSD keeps waiting until the file changes. Otherwise they follow FreeBSD 11.4's [`filt_vfsread` and `filt_vfswrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_subr.c#L5122-L5168): a read event reports the size less the descriptor's offset in `data` and fires while that is not 0; a write event always fires with `data` 0 -- [close](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) and [setsockopt](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) reach a [libSceNet](../../../core/libs/prx/libSceNet/Export.cpp) socket through hooks libSceNet registers, as `select` polls them; the other libkernel socket calls do not. libSceNet numbers its sockets from 32 in its own table, so a CRT file descriptor and a socket can share a number; `close` then closes the socket. On the console both are one kernel descriptor space +- [close](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) and [setsockopt](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) reach a [libSceNet](../../../core/libs/prx/libSceNet/Export.cpp) socket through hooks libSceNet registers, as `select` and `poll` poll them and `fcntl` answers `F_GETFL` and `F_SETFL`, failing other commands with `EINVAL`; the other libkernel socket calls do not. libSceNet numbers its sockets from 32 in its own table, so a CRT file descriptor and a socket can share a number; `close` then closes the socket. On the console both are one kernel descriptor space - [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards. Guest scalar transfers, positioned transfers, vectored transfers and seeks share a position lock across duplicated descriptors; a vector holds it for the whole operation. Independently opened descriptors and other processes do not share that lock, so vector transfers over their overlapping file ranges are not coordinated. Handles that are not disk files return `ESPIPE`. A pipe or console `readv` can return a short read when no more input is available; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done - [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards; positioned transfers, plain transfers and seeks of one descriptor are serialized per descriptor (shared across duplicated descriptors), so a concurrent transfer observes a stable file pointer - [sceKernelAioSetParam](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - the limits and `sceKernelAioInitializeParam`'s defaults (three 20-byte scheduling structs, the last without split) are measured on a PS5 (firmware 13.60), where a null struct crashes both calls; here both throw. The scheduling settings are not used: `sceKernelAioInitializeImpl` ignores the struct and requests run synchronously on submit @@ -218,7 +218,7 @@ - [send, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - send and sendto without an address on an unconnected datagram socket map Windows `WSAENOTCONN` to FreeBSD's `EDESTADDRREQ` (39). - [bind, connect, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) check a socket address length as FreeBSD 11.4's [`getsockaddr`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_syscalls.c) and the `sa_len` test of `tcp_usr_connect` and `in_pcbconnect` do: below 2 or an `AF_INET` address below 16 or an `AF_INET6` address below 28 bytes fails with `EINVAL`, above 255 with `ENAMETOOLONG`. `sendto` does not check an address longer than `sockaddr_in` on an `AF_INET` datagram socket, where `udp_output` answers `EINVAL` unless the socket has a local address and port and the destination is neither `INADDR_ANY` nor `INADDR_BROADCAST`, nor on an unconnected stream socket, which FreeBSD connects implicitly through `tcp_connect` and its length test. An address of another family still fails with `EAFNOSUPPORT`, as the console's behaviour for it is not known. None of this was measured on a console. - [read, write and vector I/O](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. `EPIPE` from a write raises the host's `SIGPIPE` on Linux unless the socket has `SO_NOSIGPIPE` set, and the guest cannot ignore it: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; nothing is raised on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor -- [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` +- [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `sceKernelClose` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` - [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number - [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A kqueue is ready for `POLLIN` and `POLLRDNORM` while one of its events is triggered and for no other event, as FreeBSD 11.4's [`kqueue_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c#L1900-L1924) answers. A kqueue cannot be waited on either, so on every host `poll` checks it every millisecond and waits on the other descriptors in slices of 1 ms. A kqueue closed during the wait reports `POLLNVAL`, and one whose number was reused is polled again with the rest of the timeout, as FreeBSD 11.4's [`pollrescan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1499-L1519) looks the descriptor up again. A close that has already removed the kqueue but not yet closed its host descriptor is seen as a descriptor of unknown kind, which throws. A regular file, a directory or a file read from app0.zar is always ready for `POLLIN`, `POLLRDNORM` and `POLLOUT` and never for `POLLPRI`, `POLLRDBAND` or `POLLWRBAND`, as FreeBSD 11.4's [`vop_stdpoll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_default.c#L563-L574) and `vop_nopoll` answer every event `poll` accepts; which of the two the console's filesystems use is not known, and they agree on these events. `/dev/random` and `/dev/urandom` follow FreeBSD 11.4's [`randomdev_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/dev/random/randomdev.c#L274-L285) for a seeded device: a request with `POLLIN` or `POLLRDNORM` gets only those bits back, and any other request gets every requested bit. Open descriptors 0 to 2 follow the console model `select` uses: descriptor 0 never reports an event and descriptors 1 and 2 report `POLLOUT`, whatever the host attaches to them. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket, a pipe, a file, a directory, the random device nor descriptor 0 to 2 (other devices) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket, pipe or kqueue to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked - [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) on Windows reports a guest stream socket whose peer closed as readable and adds `POLLHUP` only once the socket has shut its own sending side, as FreeBSD 11.4's [`sopoll_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3080-L3086) does; `WSAPoll` reports `POLLHUP` alone in both cases. A reset or refused connection (`POLLHUP` with `POLLERR`) keeps `WSAPoll`'s flags, a libSceNet socket keeps them too, and a call that asks for no event still returns early on the peer's close with nothing reported. Not measured on a console. From e49a927811fefea42be1df2c41e3387f2da340a5 Mon Sep 17 00:00:00 2001 From: Bram910 Date: Sat, 24 Oct 2026 21:31:30 +0200 Subject: [PATCH 242/428] test(tools): exercise Unicode advisor output --- tools/tests/test_suggest_patches.py | 47 ++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/tools/tests/test_suggest_patches.py b/tools/tests/test_suggest_patches.py index e6402342a8..4f9f5677ab 100644 --- a/tools/tests/test_suggest_patches.py +++ b/tools/tests/test_suggest_patches.py @@ -66,25 +66,37 @@ class PatchAdvisorTests(unittest.TestCase): def test_relinker_output_is_utf8_independent_of_host_encoding(self): actual_run = subprocess.run for diagnostic in ("caf\u00e9 \u65e5\u672c\u8a9e".encode(), b"invalid byte \xff"): - with self.subTest(diagnostic=diagnostic): - stdout = VERIFIED.encode() + diagnostic + b"\n" - stderr = diagnostic + b"\n" - captured = [] + for status in (0, 2): + with self.subTest(diagnostic=diagnostic, status=status): + stdout = VERIFIED.encode() + diagnostic + b"\n" + stderr = diagnostic + b"\n" + captured = [] + output, errors = io.StringIO(), io.StringIO() - def verify(command, **options): - script = f"import os; os.write(1, {stdout!r}); os.write(2, {stderr!r})" - options.setdefault("encoding", "ascii") - result = actual_run([sys.executable, "-c", script], **options) - captured.append(result) - return result + def verify(command, **options): + script = f"import os; os.write(1, {stdout!r}); os.write(2, {stderr!r}); raise SystemExit({status})" + options.setdefault("encoding", "ascii") + result = actual_run([sys.executable, "-c", script], **options) + captured.append(result) + return result - with patch.object(suggest_patches.subprocess, "run", side_effect=verify): - self.assertEqual(self.advise(), 0) - self.assertEqual(captured[0].stdout, stdout.decode("utf-8", errors="replace")) - self.assertEqual(captured[0].stderr, stderr.decode("utf-8", errors="replace")) + with contextlib.redirect_stdout(output), contextlib.redirect_stderr(errors), \ + patch.object(suggest_patches.subprocess, "run", side_effect=verify): + self.assertEqual(self.advise(), status) + self.assertEqual(captured[0].stdout, stdout.decode("utf-8", errors="replace")) + self.assertEqual(captured[0].stderr, stderr.decode("utf-8", errors="replace")) + if status == 2: + self.assertIn("relinker error (exit 2)", output.getvalue()) + self.assertIn((stdout + stderr).decode("utf-8", errors="replace").strip(), output.getvalue()) + self.assertIn("No recommendation: relinker errors occurred", errors.getvalue()) @patch.object(suggest_patches.subprocess, "run") def test_output_streams_use_utf8(self, run): + catalog = self.root / "caf\u00e9" + self.catalog.rename(catalog) + self.catalog = catalog + self.param = self.root / "caf\u00e9.json" + self.param.write_text(json.dumps({"titleId": "TEST00001", "contentVersion": "01.000.000"})) stdout = io.TextIOWrapper(io.BytesIO(), encoding="ascii") stderr = io.TextIOWrapper(io.BytesIO(), encoding="ascii") self.addCleanup(stdout.close) @@ -92,6 +104,13 @@ class PatchAdvisorTests(unittest.TestCase): run.return_value = subprocess.CompletedProcess([], 0, VERIFIED, "") with patch.object(suggest_patches.sys, "stdout", stdout), patch.object(suggest_patches.sys, "stderr", stderr): self.assertEqual(self.advise(), 0) + stdout.flush() + self.assertIn(b"Recommended:", stdout.buffer.getvalue()) + self.assertIn(str(self.catalog).encode("utf-8"), stdout.buffer.getvalue()) + self.param.unlink() + self.assertEqual(self.advise(), 2) + stderr.flush() + self.assertIn(self.param.name.encode("utf-8"), stderr.buffer.getvalue()) for stream in (stdout, stderr): self.assertEqual(stream.encoding, "utf-8") self.assertEqual(stream.errors, "backslashreplace") From 21127fe316db580492921055ed3ae18e42876b7c Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Sat, 24 Oct 2026 21:03:17 +0200 Subject: [PATCH 243/428] fix(relinker): reject guest TLS symbols in special sections GuestImageReader rejected a defined symbol whose section index is SHN_LORESERVE (0xff00) or higher, SHN_ABS excepted, only when it was not STT_TLS, so a TLS symbol in SHN_ABS, SHN_COMMON, 0xff00 or SHN_XINDEX was accepted. The Linux output kept it in .dynsym with that section index, Windows and macOS failed only for an export outside SHN_ABS, and both wrote st_value plus addend for a DTPOFF64 relocation against a local one. The gABI gives a defined STT_TLS symbol its assigned TLS offset and no meaning in a special section index, so every such symbol now fails with "Unsupported special symbol section" before the TLS bounds check. SHN_ABS stays accepted for other symbol types. --- .../relinker/src/guest/GuestImageReader.cpp | 6 ++--- .../tests/test_guest_absolute_symbols.py | 27 ++++++++++++++++--- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index 6010508de0..699f47df2a 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -189,11 +189,9 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector const auto [found, inserted] = exports.emplace(name, image.Symbols.size()); if (name.empty() || (!inserted && (!windows || !IdenticalOrdinaryFunctionExport(image.Symbols[found->second], symbol)))) fail("Duplicate or empty export after stripping #: " + name); } + if (section >= 0xff00 && (section != AbsoluteSection || (info & 15) == 6)) fail("Unsupported special symbol section: " + name); if ((info & 15) == 6 && section != 0 && (tls == nullptr || value > tls->MemorySize || size > tls->MemorySize - value)) fail("TLS symbol exceeds TLS segment: " + name); - if (section != 0 && section != AbsoluteSection && (info & 15) != 6) { - if (section >= 0xff00) fail("Unsupported special symbol section: " + name); - mapped(value, std::max(size, 1), (info & 15) == 2 ? 1 : 0); - } + if (section != 0 && section != AbsoluteSection && (info & 15) != 6) mapped(value, std::max(size, 1), (info & 15) == 2 ? 1 : 0); if (image.Dynamic.DynStrData.size() > std::numeric_limits::max()) fail("String table too large"); Io::WriteU32(image.Dynamic.DynSymData, offset, static_cast(image.Dynamic.DynStrData.size())); Io::AppendString(image.Dynamic.DynStrData, name); diff --git a/core/relinker/relinker/tests/test_guest_absolute_symbols.py b/core/relinker/relinker/tests/test_guest_absolute_symbols.py index 77a57f1fba..67fc6810a2 100644 --- a/core/relinker/relinker/tests/test_guest_absolute_symbols.py +++ b/core/relinker/relinker/tests/test_guest_absolute_symbols.py @@ -15,6 +15,7 @@ VALUE = 0x1234 SIZE = 0x10 ADDEND = 0x10 TARGET = 0x2320 +TLS_SECTIONS = (ABSOLUTE, COMMON, 0xFF00, 0xFFFF) def module_with_section(section, size=0): @@ -25,6 +26,16 @@ def module_with_section(section, size=0): return image +def tls_module(section, binding): + image = module_with_symbol(True) + struct.pack_into(" Date: Sat, 24 Oct 2026 21:02:47 +0200 Subject: [PATCH 244/428] fix(relinker): reject non-TLS guest relocations bound to a guest TLS export A guest STT_NOTYPE import that resolved to another guest's STT_TLS export was accepted when R_X86_64_64, R_X86_64_GLOB_DAT or R_X86_64_JUMP_SLOT referenced it, and every target filled the slot with an address that is not the variable. The gABI lets only TLS relocations reference an STT_TLS symbol; the reader already rejects such relocations against a TLS-typed import, and the builder rejects OBJECT and FUNC imports of a TLS export as a type mismatch. The builder now also rejects a referenced NOTYPE import of a guest TLS export with "Non-TLS relocation against guest TLS export". These inputs exit 2 on Linux, Windows and macOS instead of producing outputs; unreferenced NOTYPE entries and TLS relocations are unchanged. --- core/relinker/CMakeLists.txt | 3 + .../relinker/src/guest/GuestModuleBuilder.cpp | 1 + .../tests/test_guest_tls_export_references.py | 89 +++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 core/relinker/relinker/tests/test_guest_tls_export_references.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 55968ce9c2..ac5af50abc 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -616,6 +616,9 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_syscall_trap.py $) add_test(NAME guest_tls COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_tls.py $) + add_test(NAME guest_tls_export_references COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_tls_export_references.py $) + set_tests_properties(guest_tls_export_references PROPERTIES TIMEOUT 30) add_test(NAME guest_needed_modules COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_needed_modules.py $) add_test(NAME guest_module_dir COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 31dc35aa0d..7c2110ae58 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -289,6 +289,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } if (provider != nullptr) { if ((symbol.Info & 15) != 0 && (symbol.Info & 15) != provider->Type) throw Domain::RelinkerException("Guest import/export type mismatch: " + symbol.Name); + if (provider->Type == 6 && (symbol.Info & 15) == 0 && referencedSymbols.contains(symbolIndex)) throw Domain::RelinkerException("Non-TLS relocation against guest TLS export: " + symbol.Name); if (provider->Provider != index && referencedSymbols.contains(symbolIndex)) dependencies[index].insert(provider->Provider); } else if (windows && (symbol.Info & 15) == 6) throw Domain::RelinkerException("Windows guest TLS import requires a guest TLS export: " + symbol.Name); if (provider == nullptr) systemImports[index].insert(symbolIndex); diff --git a/core/relinker/relinker/tests/test_guest_tls_export_references.py b/core/relinker/relinker/tests/test_guest_tls_export_references.py new file mode 100644 index 0000000000..d5401da429 --- /dev/null +++ b/core/relinker/relinker/tests/test_guest_tls_export_references.py @@ -0,0 +1,89 @@ +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from guest_import_fixtures import dynamic +from test_guest_intel_trampolines import PLAIN_SITE, guest_fixture, main_fixture + + +STRINGS = b"\0tls_value\0a.prx\0" +SLOT = 0x2340 +GLOB_DAT = 6 +JUMP_SLOT = 7 +DTPMOD64 = 16 +DTPOFF64 = 17 +REJECTED = "Non-TLS relocation against guest TLS export: tls_value" +MISMATCH = "Guest import/export type mismatch: tls_value" + + +def symbol_table(image, info, section, size, value=0): + image[0x800:0x800 + len(STRINGS)] = STRINGS + struct.pack_into(" Date: Sat, 24 Oct 2026 21:03:41 +0200 Subject: [PATCH 245/428] fix(relinker): require a non-empty PT_TLS for executable TLS relocations An executable's local TPOFF64, DTPOFF64 or DTPMOD64 relocation passed whenever a PT_TLS header existed, so the Linux targets emitted it next to a PT_TLS with p_memsz 0, while the Windows and macOS relocation builder rejected the same input. glibc's ld.so sets up the main program's TLS module only for a PT_TLS with p_memsz > 0, so such a relocation has no block to resolve against. The pipeline now requires a PT_TLS with a nonzero p_memsz and fails on every target with "TLS relocation in an executable without a non-empty PT_TLS segment". Relocations against imported TLS symbols are unchanged. --- .../src/pipeline/RelinkerPipeline.cpp | 8 +++---- .../tests/test_executable_tls_relocations.py | 24 +++++++++++++++---- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 77044446d2..b23249be0d 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -312,8 +312,8 @@ RelinkResult RelinkerPipeline::Relink() { return type == R_X86_64_DTPMOD64 || type == R_X86_64_DTPOFF64 || type == R_X86_64_TPOFF64; }; - const bool hasTlsSegment = std::any_of(programHeaders.begin(), programHeaders.end(), - [](const ProgramHeader& ph) { return ph.Type == PT_TLS; }); + const bool hasNonEmptyTlsSegment = std::any_of(programHeaders.begin(), programHeaders.end(), + [](const ProgramHeader& ph) { return ph.Type == PT_TLS && ph.MemorySize != 0; }); const auto definedTlsSymbolValue = [&](const std::uint32_t symIdx, const FileByteOffset pos) -> std::optional { const FileByteOffset symOff = symbolEntryPos(symIdx); @@ -545,8 +545,8 @@ RelinkResult RelinkerPipeline::Relink() { continue; offset = *value; } - if (!hasTlsSegment) - throw RelinkerException("TLS relocation in an executable without a PT_TLS segment", pos); + if (!hasNonEmptyTlsSegment) + throw RelinkerException("TLS relocation in an executable without a non-empty PT_TLS segment", pos); if (relType == R_X86_64_DTPMOD64 && rAddend != 0) throw RelinkerException("DTPMOD64 relocation has a nonzero addend", pos); const auto addend = relType == R_X86_64_DTPMOD64 ? 0 : static_cast(offset + static_cast(rAddend)); diff --git a/core/relinker/relinker/tests/test_executable_tls_relocations.py b/core/relinker/relinker/tests/test_executable_tls_relocations.py index 8f9db5e449..65652edcc1 100644 --- a/core/relinker/relinker/tests/test_executable_tls_relocations.py +++ b/core/relinker/relinker/tests/test_executable_tls_relocations.py @@ -31,15 +31,16 @@ TLS_NAME, DATA_NAME, IMPORT_NAME = 1, 5, 10 NULL, TLS_SYMBOL, DATA_SYMBOL, IMPORTED = 0, 1, 2, 3 -def image(relocations, tls=True, symbol_bytes=None): - """The load-alignment fixture with a writable segment (vaddr 0x1000) holding a GOT and an 8-byte - TLS template, a symbol table and the given (offset, type, symbol, addend) relocations.""" +def image(relocations, tls=True, symbol_bytes=None, tls_memsz=TLS_MEMSZ): + """The load-alignment fixture with a writable segment (vaddr 0x1000) holding a GOT and a PT_TLS of + tls_memsz bytes with an up to 8-byte template, a symbol table and the given (offset, type, symbol, + addend) relocations.""" data = fixture() struct.pack_into(" Date: Sat, 24 Oct 2026 22:03:53 +0200 Subject: [PATCH 246/428] fix(tools): shorten NID database temporary names --- tools/nid_names.py | 2 +- tools/tests/test_nid_names.py | 54 +++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/tools/nid_names.py b/tools/nid_names.py index a7d1841005..8f4e3ffd34 100644 --- a/tools/nid_names.py +++ b/tools/nid_names.py @@ -51,7 +51,7 @@ def load_db(path): if not path.exists(): path.parent.mkdir(parents=True, exist_ok=True) print("downloading NID database to %s ..." % path, file=sys.stderr) - with tempfile.NamedTemporaryFile(dir=path.parent, prefix=path.name + ".", suffix=".tmp", delete=False) as download: + with tempfile.NamedTemporaryFile(dir=path.parent, prefix=".nid-db-", suffix=".tmp", delete=False) as download: temporary = Path(download.name) try: urllib.request.urlretrieve(DB_URL, str(temporary)) diff --git a/tools/tests/test_nid_names.py b/tools/tests/test_nid_names.py index f5ccf2945d..386d547827 100644 --- a/tools/tests/test_nid_names.py +++ b/tools/tests/test_nid_names.py @@ -15,6 +15,60 @@ import nid_names class DatabaseCacheTests(unittest.TestCase): + def long_cache_path(self, directory): + root = Path(directory) + if os.name == "nt": + root = Path("\\\\?\\" + str(root.resolve())) + limit = os.pathconf(root, "PC_NAME_MAX") if os.name == "posix" else 255 + cache = root / ("n" * (limit - len(".csv")) + ".csv") + cache.touch() + cache.unlink() + return cache + + def test_valid_long_cache_filename_can_be_downloaded(self): + with tempfile.TemporaryDirectory() as directory: + cache = self.long_cache_path(directory) + + def complete(url, filename): + self.assertEqual(url, nid_names.DB_URL) + self.assertEqual(Path(filename).parent, cache.parent) + self.assertFalse(cache.exists()) + Path(filename).write_text("nid1 sceComplete\n", encoding="utf-8") + + try: + with patch.object(nid_names.urllib.request, "urlretrieve", side_effect=complete) as download: + self.assertEqual(nid_names.load_db(cache), {"nid1": "sceComplete"}) + download.assert_called_once() + self.assertEqual(list(cache.parent.iterdir()), [cache]) + with patch.object(nid_names.urllib.request, "urlretrieve") as download: + self.assertEqual(nid_names.load_db(cache), {"nid1": "sceComplete"}) + download.assert_not_called() + finally: + cache.unlink(missing_ok=True) + + def test_long_cache_filename_preserves_interruption_cleanup_and_retry(self): + with tempfile.TemporaryDirectory() as directory: + cache = self.long_cache_path(directory) + + def interrupted(url, filename): + Path(filename).write_text("nid1 scePartial\n", encoding="utf-8") + raise URLError("connection interrupted") + + def complete(url, filename): + Path(filename).write_text("nid1 sceComplete\n", encoding="utf-8") + + try: + with patch.object(nid_names.urllib.request, "urlretrieve", side_effect=interrupted): + with self.assertRaises(URLError): + nid_names.load_db(cache) + self.assertFalse(cache.exists()) + self.assertEqual(list(cache.parent.iterdir()), []) + with patch.object(nid_names.urllib.request, "urlretrieve", side_effect=complete): + self.assertEqual(nid_names.load_db(cache), {"nid1": "sceComplete"}) + self.assertEqual(list(cache.parent.iterdir()), [cache]) + finally: + cache.unlink(missing_ok=True) + def test_empty_symbol_filters_do_not_open_database(self): with tempfile.TemporaryDirectory() as directory: for option in ("--name", "--symbol", "--na", "--sym"): From 1fbd44b66f942bc9d5aa6303b84a6c64a040ef40 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 22 Oct 2026 04:53:25 -0400 Subject: [PATCH 247/428] fix(libc): format %a like the console on Windows The Windows printf family handed %a and %A to the host, whose UCRT prints a subnormal unnormalised (0x0.0000000000001p-1022) and pads with zeros even when the - flag is given (%-010a of 1.5 gave 0x001.8p+0). The libc.prx that titles ship normalises subnormals (0x1p-1074), rounds a shorter precision half up without renormalising a carry, and lets - override 0, as C requires. Finite doubles are now formatted here, checked against three titles' copies of libc.prx run side by side. Co-Authored-By: Claude Opus 5.5 --- .../prx/libc/include/WindowsFormatting.hpp | 77 ++++++++++++++++++- core/libs/tests/Formatting.cpp | 31 +++++++- docs/dev/debt/libc.md | 1 + 3 files changed, 106 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index f0ee4f771d..d557d927ea 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -3,6 +3,7 @@ #include "SceTypes.hpp" #include +#include #include #include #include @@ -160,6 +161,72 @@ inline void AppendUtf8(std::string& utf8, char32_t code) { } } +inline std::string HexFloat(const std::string& spec, char conversion, double value) { + bool left = false, plus = false, space = false, alternate = false, zero = false; + std::size_t index = 1; + for (; index < spec.size() && std::strchr("-+ #0", spec[index]); ++index) { + left |= spec[index] == '-'; + plus |= spec[index] == '+'; + space |= spec[index] == ' '; + alternate |= spec[index] == '#'; + zero |= spec[index] == '0'; + } + std::size_t width = 0; + for (; index < spec.size() && spec[index] >= '0' && spec[index] <= '9'; ++index) + width = width <= static_cast(INT_MAX) / 10 ? width * 10 + static_cast(spec[index] - '0') : static_cast(INT_MAX); + int precision = -1; + if (index < spec.size() && spec[index] == '.') { + precision = 0; + for (++index; index < spec.size(); ++index) + precision = precision <= INT_MAX / 10 ? precision * 10 + (spec[index] - '0') : INT_MAX; + } + const bool upper = conversion == 'A'; + const std::uint64_t bits = std::bit_cast(value); + int exponent = static_cast((bits >> 52) & 0x7ff); + std::uint64_t fraction = bits & ((std::uint64_t{1} << 52) - 1); + std::uint64_t lead = 1; + if (exponent != 0) { + exponent -= 1023; + } else if (fraction == 0) { + lead = 0; + } else { + exponent = -1022; + while ((fraction & (std::uint64_t{1} << 52)) == 0) { + fraction <<= 1; + --exponent; + } + fraction &= (std::uint64_t{1} << 52) - 1; + } + int digits = 13; + if (precision >= 0 && precision < 13) { + const int dropped = (13 - precision) * 4; + const std::uint64_t remainder = fraction & ((std::uint64_t{1} << dropped) - 1); + fraction >>= dropped; + if (remainder >= std::uint64_t{1} << (dropped - 1) && ++fraction >> (precision * 4)) { + fraction &= (std::uint64_t{1} << (precision * 4)) - 1; + ++lead; + } + digits = precision; + } else if (precision < 0) { + for (; digits > 0 && (fraction & 0xf) == 0; --digits) fraction >>= 4; + } + const char* hex = upper ? "0123456789ABCDEF" : "0123456789abcdef"; + std::string body(1, hex[lead]); + if (digits > 0 || precision > 13 || alternate) body += '.'; + for (int digit = digits - 1; digit >= 0; --digit) body += hex[(fraction >> (digit * 4)) & 0xf]; + if (precision > 13) body.append(static_cast(precision - 13), '0'); + body += upper ? 'P' : 'p'; + body += exponent < 0 ? '-' : '+'; + body += std::to_string(exponent < 0 ? -exponent : exponent); + std::string prefix = (bits >> 63) != 0 ? "-" : plus ? "+" : space ? " " : ""; + prefix += upper ? "0X" : "0x"; + const std::size_t length = prefix.size() + body.size(); + if (length >= width) return prefix + body; + const std::string padding(width - length, left || !zero ? ' ' : '0'); + if (left) return prefix + body + padding; + return zero ? prefix + padding + body : padding + prefix + body; +} + inline std::string PaddedStringSpec(const std::string& spec) { std::string result = "%"; std::size_t index = 1; @@ -378,8 +445,14 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo } else { if (!length.empty() && length != "l") throw std::invalid_argument("Invalid floating length"); const double value = args.Next(); - if (std::isnan(value)) output.Value(PaddedStringSpec(spec), NanText(spec, std::signbit(value), conversion < 'a').c_str()); - else output.Value(spec + conversion, value); + if (std::isnan(value)) { + output.Value(PaddedStringSpec(spec), NanText(spec, std::signbit(value), conversion < 'a').c_str()); + } else if ((conversion == 'a' || conversion == 'A') && std::isfinite(value)) { + const std::string text = HexFloat(spec, conversion, value); + output.Append(text.data(), text.size()); + } else { + output.Value(spec + conversion, value); + } } } else if (conversion == 'c' && (length.empty() || length == "h")) { output.Value(spec + conversion, args.Next()); diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index ea4b2ecaa8..3757c4d141 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -413,4 +413,33 @@ static void CheckAllocatingFormat() { free_nid_postfix(allocated); } -int main() { CheckSizeLimits(); RunChecks(); CheckAllocatingFormat(); return CheckWidePrecision() ? 0 : 1; } +static void RequireHexFloat(const char* format, double value, const char* expected) { + char buffer[64]; + const int written = snprintf_nid_postfix(buffer, sizeof(buffer), format, value); + if (written != static_cast(std::strlen(expected)) || std::strcmp(buffer, expected) != 0) { + std::fprintf(stderr, "%s of %a gave \"%s\", expected \"%s\"\n", format, value, buffer, expected); + Require(false); + } +} + +static void CheckHexFloat() { + RequireHexFloat("[%-010a]", 1.5, "[0x1.8p+0 ]"); + RequireHexFloat("[%-0#12A]", 0.75, "[0X1.8P-1 ]"); + RequireHexFloat("[%0-10.2a]", 1.0, "[0x1.00p+0 ]"); + RequireHexFloat("[%010a]", -1.5, "[-0x01.8p+0]"); + RequireHexFloat("[%a]", 0x1p-1074, "[0x1p-1074]"); + RequireHexFloat("[%a]", 0x0.fffffffffffffp-1022, "[0x1.ffffffffffffep-1023]"); + RequireHexFloat("[%.3a]", 0x0.fffffffffffffp-1022, "[0x2.000p-1023]"); + RequireHexFloat("[%A]", 0x0.012688b70e62bp-1022, "[0X1.2688B70E62BP-1030]"); + RequireHexFloat("[%.0a]", 0x0.012688b70e62bp-1022, "[0x1p-1030]"); + RequireHexFloat("[%.1a]", 0x1.08p0, "[0x1.1p+0]"); + RequireHexFloat("[%.0a]", 0x1.8p0, "[0x2p+0]"); + RequireHexFloat("[%#.0a]", 1.0, "[0x1.p+0]"); + RequireHexFloat("[%.20a]", 0.1, "[0x1.999999999999a0000000p-4]"); + RequireHexFloat("[% a]", -0.0, "[-0x0p+0]"); + char buffer[32]; + Require(snprintf_nid_postfix(buffer, sizeof(buffer), "[%*a]", -12, 1.5) == 14 && std::strcmp(buffer, "[0x1.8p+0 ]") == 0); + Require(snprintf_nid_postfix(buffer, 6, "%a", 1.5) == 8 && std::strcmp(buffer, "0x1.8") == 0); +} + +int main() { CheckSizeLimits(); RunChecks(); CheckHexFloat(); CheckAllocatingFormat(); return CheckWidePrecision() ? 0 : 1; } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 2d4e0fc01d..9af184d20e 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -136,6 +136,7 @@ - [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [c16rtomb, c32rtomb, mbrtoc16 and mbrtoc32](../../../core/libs/prx/libc/src/Multibyte.cpp) (libSceLibcInternal, implemented in libc) - a null state, which the console dereferences, throws. Otherwise they follow the console's `libSceLibcInternal.sprx` (0x7D480 to 0x7D920), read from disassembly: UTF-8 whatever the locale, up to six bytes per sequence, with no surrogate or 0x10FFFF check when encoding. The state holds the value in its first eight bytes and the expected byte count (0x10 for a pending low surrogate, 1 for a stored high one) as 16 bits at 0xA. `mbrtoc16` builds a low surrogate from the value the state last held, `c16rtomb` returns -1 without `errno` for an unpaired high surrogate, and `EILSEQ` is 86. On a PS5 (13.60) the system library's default locale converts UTF-8 in `wcrtomb`, `wctomb`, `mbrtowc` and `mblen`; a title's bundled `libc.prx` does too until `setlocale` installs a table ([disassembly](https://github.com/boykopovar/AnyPS5/pull/8855#issuecomment-6335267758)) - [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) follows a payload process on a PS5 (firmware 13.60): each 16-bit unit of a `%ls` or `%lc` argument is encoded alone, so a surrogate pair gives two 3-byte sequences; a NaN keeps its sign and the `+` and space flags, and `0` pads it with spaces; argument positions above 99 work (up to 400 measured; above 65535 throw here); an unknown conversion such as `%m` or `%k` prints `%` and its letter, padded to the width, without taking an argument. The guest's `wcrtomb` and locales are not used. On Linux the formats without those conversions go to glibc, which prints NaN the same way. The `_s` forms throw for an unknown conversion, `%m` included, which was not measured +- [%a and %A](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) on Windows format a finite double as the `libc.prx` that titles ship does, instead of through the host: a subnormal is normalised (`0x1p-1074`), a shorter precision rounds half up on the dropped hexadecimal digits and a carry stays in the leading digit (`%.3a` of `0x0.fffffffffffffp-1022` is `0x2.000p-1023`), and `-` overrides `0`. Checked by running the copies of PPSA13934, PPSA07923 and PPSA05916 side by side on 7 million random formats, not measured on a console. Infinities and `long double` still go to the host; NaN prints without its sign, as in FreeBSD, where those copies print `-nan`. Linux hands `%a` to glibc, which prints a subnormal unnormalised - [%p and %lp](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) follow the console's `libSceLibcInternal.sprx` measured from a homebrew payload on firmware 12.70 ([#7549](https://github.com/boykopovar/AnyPS5/issues/7549)), not FreeBSD: bare lowercase hex with no `0x` prefix even under `#`, `0` for a null pointer, `+` and the space flag ignored, the width counting only the digits, `-` padding on the right, `0` padding with zeros when there is no precision, the precision as the minimum number of digits, nothing for a null pointer under `%.0p`, and `l` accepted. Only `snprintf` and `swprintf` of that module were measured, from a payload process rather than a title process, so a width beside `%.0p`, the `0` flag beside a precision (which C17 ignores) and `%.0p` of a non-null pointer follow this formatter's own rules and are not measured; `swprintf` was not run with `%lp`, so the wide family still refuses that conversion and returns -1. Linux routes `%p` and `%lp` to this formatter because glibc prints the FreeBSD form and `(nil)` for a null pointer, which means a format that combines `%p` with a conversion this formatter refuses, such as an argument position above 99, throws there instead of reaching glibc - [collate and collate _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build the facet for the C locale only and throw for any other locale name, the wide entry point following the narrow one it sits next to. The wide facet carries its own vtable, whose `compare`, `transform` and `hash` take the guest's 16-bit characters, so the length of a range is a count of characters: `compare` orders by 16-bit value (the C locale, as Dinkumware's `char_traits::compare` does), `transform` copies the characters into the result and records size and capacity in 16-bit elements (inline capacity 7, heap capacity `length | 7`, `(capacity + 1) * 2` bytes including the terminator), following Dinkumware's [VC12 `basic_string`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/xstring#L500-L531), and `hash` mixes one whole character per round, so wide text below 0x100 hashes like its narrow spelling and a character above 0xff hashes as one value instead of the two bytes it holds. The console's `libc.prx` exports both `_Getcat` (NIDs BSVJqITGCyI and 2wz4rthdiy8) with identical 0x19d-byte bodies, believed to build the same 0x20-byte facet; the console bodies themselves are unreadable (obfuscated dynsym, and the text of the relinked `libc.prx` is zeroed), so the wide entries follow the narrow algorithm and the C locale instead of the console's wide code, and no console run checks the two. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. The narrow result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware. No module of the PPSA59322, PPSA76677 or PPSA97358 dumps in this tree imports either `_Getcat`, so neither has been exercised by a title. - [codecvt, numpunct, num_put and time_put _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build facets matching a bundled console `libc.prx` (dynsym decoded by NID, vtables disassembled): categories 2, 4, 4 and 5, sizes 0x10, 0x30, 0x10 and 0xb8. `codecvt` reproduces the console virtuals (`noconv` conversions, `length` the shorter range); `numpunct` copies its C-locale table (`""`, `.`, `,`, `false`, `true`). `num_put`/`time_put` formatting virtuals throw rather than fabricate output, and `time_put`'s `_Locinfo` table is unpopulated — the console's `do_put` bodies were not mirrored. The facets share the no-op `destroy`/`deleteObject`/`retain`/`release` base used for `collate`, so `numpunct`'s static C-locale strings are never freed. Unlike `collate`, these `_Getcat` entries ignore the locale argument and always build the C-locale facet; the console's behaviour for a non-C name is unverified. `std::clog` and `std::wclog` are 0x400-byte zeroed objects like the rest of the `NSTDSTR` stream set (the console's `libc.prx` sizes `std::clog` at 96 bytes, measured from its dynsym `st_size`), and the `numpunct::id` and `time_put::id` objects are 8-byte zeros like the other facet ids. From e2d2d5171be88123fbbcc4d9e2d50a906cc66c9f Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 22 Oct 2026 06:52:05 -0400 Subject: [PATCH 248/428] fix(libc): reject overflowing hex float precision --- core/libs/prx/libc/include/WindowsFormatting.hpp | 8 ++++++-- core/libs/tests/Formatting.cpp | 12 ++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index d557d927ea..8880c18ba1 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -177,8 +177,12 @@ inline std::string HexFloat(const std::string& spec, char conversion, double val int precision = -1; if (index < spec.size() && spec[index] == '.') { precision = 0; - for (++index; index < spec.size(); ++index) - precision = precision <= INT_MAX / 10 ? precision * 10 + (spec[index] - '0') : INT_MAX; + for (++index; index < spec.size(); ++index) { + const int digit = spec[index] - '0'; + if (precision > (INT_MAX - digit) / 10) + throw std::overflow_error("Floating precision exceeds int range"); + precision = precision * 10 + digit; + } } const bool upper = conversion == 'A'; const std::uint64_t bits = std::bit_cast(value); diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index 3757c4d141..11e06c2cca 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -440,6 +440,18 @@ static void CheckHexFloat() { char buffer[32]; Require(snprintf_nid_postfix(buffer, sizeof(buffer), "[%*a]", -12, 1.5) == 14 && std::strcmp(buffer, "[0x1.8p+0 ]") == 0); Require(snprintf_nid_postfix(buffer, 6, "%a", 1.5) == 8 && std::strcmp(buffer, "0x1.8") == 0); +#ifdef _WIN32 + for (const char* format : {"%.2147483648a", "%.2147483649A", "%.9999999999a", + "%.9999999999999999999999999999999999999999A", "%.0002147483648a", "%-+20.2147483648A"}) { + bool rejected = false; + try { + snprintf_nid_postfix(buffer, sizeof(buffer), format, 1.5); + } catch (const std::overflow_error&) { + rejected = true; + } + Require(rejected); + } +#endif } int main() { CheckSizeLimits(); RunChecks(); CheckHexFloat(); CheckAllocatingFormat(); return CheckWidePrecision() ? 0 : 1; } From a8c040004f73bbee0e2bcef5a4c469bee84d429f Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 22 Oct 2026 07:08:47 -0400 Subject: [PATCH 249/428] fix(libc): reject oversized hex float output before allocation --- .../prx/libc/include/WindowsFormatting.hpp | 29 ++++++++++++------- core/libs/tests/Formatting.cpp | 3 +- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index 8880c18ba1..55810124c9 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -172,8 +172,12 @@ inline std::string HexFloat(const std::string& spec, char conversion, double val zero |= spec[index] == '0'; } std::size_t width = 0; - for (; index < spec.size() && spec[index] >= '0' && spec[index] <= '9'; ++index) - width = width <= static_cast(INT_MAX) / 10 ? width * 10 + static_cast(spec[index] - '0') : static_cast(INT_MAX); + for (; index < spec.size() && spec[index] >= '0' && spec[index] <= '9'; ++index) { + const auto digit = static_cast(spec[index] - '0'); + if (width > (static_cast(INT_MAX) - digit) / 10) + throw std::overflow_error("Formatted output exceeds INT_MAX"); + width = width * 10 + digit; + } int precision = -1; if (index < spec.size() && spec[index] == '.') { precision = 0; @@ -215,16 +219,21 @@ inline std::string HexFloat(const std::string& spec, char conversion, double val for (; digits > 0 && (fraction & 0xf) == 0; --digits) fraction >>= 4; } const char* hex = upper ? "0123456789ABCDEF" : "0123456789abcdef"; - std::string body(1, hex[lead]); - if (digits > 0 || precision > 13 || alternate) body += '.'; - for (int digit = digits - 1; digit >= 0; --digit) body += hex[(fraction >> (digit * 4)) & 0xf]; - if (precision > 13) body.append(static_cast(precision - 13), '0'); - body += upper ? 'P' : 'p'; - body += exponent < 0 ? '-' : '+'; - body += std::to_string(exponent < 0 ? -exponent : exponent); + std::string suffix = upper ? "P" : "p"; + suffix += exponent < 0 ? '-' : '+'; + suffix += std::to_string(exponent < 0 ? -exponent : exponent); std::string prefix = (bits >> 63) != 0 ? "-" : plus ? "+" : space ? " " : ""; prefix += upper ? "0X" : "0x"; - const std::size_t length = prefix.size() + body.size(); + const auto fractionalDigits = static_cast(precision > 13 ? precision : digits); + const bool point = fractionalDigits != 0 || alternate; + const std::size_t length = prefix.size() + 1 + point + fractionalDigits + suffix.size(); + if (length > static_cast(INT_MAX)) + throw std::overflow_error("Formatted output exceeds INT_MAX"); + std::string body(1, hex[lead]); + if (point) body += '.'; + for (int digit = digits - 1; digit >= 0; --digit) body += hex[(fraction >> (digit * 4)) & 0xf]; + if (precision > 13) body.append(static_cast(precision - 13), '0'); + body += suffix; if (length >= width) return prefix + body; const std::string padding(width - length, left || !zero ? ' ' : '0'); if (left) return prefix + body + padding; diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index 11e06c2cca..e37dfb2f09 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -442,7 +442,8 @@ static void CheckHexFloat() { Require(snprintf_nid_postfix(buffer, 6, "%a", 1.5) == 8 && std::strcmp(buffer, "0x1.8") == 0); #ifdef _WIN32 for (const char* format : {"%.2147483648a", "%.2147483649A", "%.9999999999a", - "%.9999999999999999999999999999999999999999A", "%.0002147483648a", "%-+20.2147483648A"}) { + "%.9999999999999999999999999999999999999999A", "%.0002147483648a", "%-+20.2147483648A", + "%2147483648a", "%999999999999999999999999a", "%.2147483647a", "%.2147483641a", "%+.2147483640A"}) { bool rejected = false; try { snprintf_nid_postfix(buffer, sizeof(buffer), format, 1.5); From 1a610d2f25eff86be00f35c55b40651eea326130 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 22 Oct 2026 07:31:36 -0400 Subject: [PATCH 250/428] fix(libc): bound hex float allocation by remaining output --- core/libs/prx/libc/include/WindowsFormatting.hpp | 6 +++--- core/libs/tests/Formatting.cpp | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index 55810124c9..e92a088f39 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -161,7 +161,7 @@ inline void AppendUtf8(std::string& utf8, char32_t code) { } } -inline std::string HexFloat(const std::string& spec, char conversion, double value) { +inline std::string HexFloat(const std::string& spec, char conversion, double value, std::size_t maximumLength = INT_MAX) { bool left = false, plus = false, space = false, alternate = false, zero = false; std::size_t index = 1; for (; index < spec.size() && std::strchr("-+ #0", spec[index]); ++index) { @@ -227,7 +227,7 @@ inline std::string HexFloat(const std::string& spec, char conversion, double val const auto fractionalDigits = static_cast(precision > 13 ? precision : digits); const bool point = fractionalDigits != 0 || alternate; const std::size_t length = prefix.size() + 1 + point + fractionalDigits + suffix.size(); - if (length > static_cast(INT_MAX)) + if (length > maximumLength || width > maximumLength) throw std::overflow_error("Formatted output exceeds INT_MAX"); std::string body(1, hex[lead]); if (point) body += '.'; @@ -461,7 +461,7 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo if (std::isnan(value)) { output.Value(PaddedStringSpec(spec), NanText(spec, std::signbit(value), conversion < 'a').c_str()); } else if ((conversion == 'a' || conversion == 'A') && std::isfinite(value)) { - const std::string text = HexFloat(spec, conversion, value); + const std::string text = HexFloat(spec, conversion, value, static_cast(INT_MAX - output.Count())); output.Append(text.data(), text.size()); } else { output.Value(spec + conversion, value); diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index e37dfb2f09..f40548444b 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -443,7 +443,8 @@ static void CheckHexFloat() { #ifdef _WIN32 for (const char* format : {"%.2147483648a", "%.2147483649A", "%.9999999999a", "%.9999999999999999999999999999999999999999A", "%.0002147483648a", "%-+20.2147483648A", - "%2147483648a", "%999999999999999999999999a", "%.2147483647a", "%.2147483641a", "%+.2147483640A"}) { + "%2147483648a", "%999999999999999999999999a", "%.2147483647a", "%.2147483641a", "%+.2147483640A", + "x%2147483647a", "xx%.2147483640a", "x%+.2147483639A"}) { bool rejected = false; try { snprintf_nid_postfix(buffer, sizeof(buffer), format, 1.5); From 00fa026aae18a36ded76e69f6ec34f7310f55096 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 22 Oct 2026 19:43:46 -0400 Subject: [PATCH 251/428] fix(libc): send %a to the guest formatter on Linux too Linux handed a format without wide arguments, %m or %p to glibc, so %a there still printed subnormals unnormalised and rounded hexadecimal digits half to even, and CheckHexFloat, which runs on every host, failed on Linux. Route %a and %A to FormatWindows on Linux as well. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Formatting.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index ad2e65acda..94d4896e1f 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -44,6 +44,7 @@ bool RequiresGuestFormatting(const char* format) { (*format == 'l' && (format[1] == 's' || format[1] == 'c' || format[1] == 'p'))) return true; while (*format && std::strchr("hljztL", *format)) ++format; if (*format && !std::strchr(LibcDetail::KnownConversions, *format)) return true; + if (*format && std::strchr("aA", *format)) return true; if (*format) ++format; } return false; From 377e8fccec35aefea73c6eaee48b16f4c8b44ece Mon Sep 17 00:00:00 2001 From: JJenkx Date: Fri, 23 Oct 2026 03:51:02 -0400 Subject: [PATCH 252/428] docs(debt): %a no longer goes to glibc on Linux The libc debt entry for %a and %A still described the Windows-only formatter and said Linux hands %a to glibc; since Linux routes %a and %A to the same formatter, describe it for both platforms. Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libc.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 9af184d20e..ee1b29fd58 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -135,8 +135,8 @@ - [wctomb_s, wcrtomb_s, mbstowcs_s and mbsrtowcs_s](../../../core/libs/prx/libc/src/BoundsChecking.cpp) (libc) follow the console's `libc.prx`, but convert in the single-byte C locale of `wctomb`, `wcrtomb` and `mbrtowc` (`MB_CUR_MAX` 1): a character above 0xFF is `EILSEQ` in `wctomb_s` and `wcrtomb_s`, and each byte is one wide character in `mbstowcs_s` and `mbsrtowcs_s`. The locale of a title's `libc.prx` was not measured. The `wcrtomb_s` path where a character does not fit `smax` and the `EILSEQ` paths of `mbstowcs_s` and `mbsrtowcs_s` are not reached - [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [c16rtomb, c32rtomb, mbrtoc16 and mbrtoc32](../../../core/libs/prx/libc/src/Multibyte.cpp) (libSceLibcInternal, implemented in libc) - a null state, which the console dereferences, throws. Otherwise they follow the console's `libSceLibcInternal.sprx` (0x7D480 to 0x7D920), read from disassembly: UTF-8 whatever the locale, up to six bytes per sequence, with no surrogate or 0x10FFFF check when encoding. The state holds the value in its first eight bytes and the expected byte count (0x10 for a pending low surrogate, 1 for a stored high one) as 16 bits at 0xA. `mbrtoc16` builds a low surrogate from the value the state last held, `c16rtomb` returns -1 without `errno` for an unpaired high surrogate, and `EILSEQ` is 86. On a PS5 (13.60) the system library's default locale converts UTF-8 in `wcrtomb`, `wctomb`, `mbrtowc` and `mblen`; a title's bundled `libc.prx` does too until `setlocale` installs a table ([disassembly](https://github.com/boykopovar/AnyPS5/pull/8855#issuecomment-6335267758)) -- [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) follows a payload process on a PS5 (firmware 13.60): each 16-bit unit of a `%ls` or `%lc` argument is encoded alone, so a surrogate pair gives two 3-byte sequences; a NaN keeps its sign and the `+` and space flags, and `0` pads it with spaces; argument positions above 99 work (up to 400 measured; above 65535 throw here); an unknown conversion such as `%m` or `%k` prints `%` and its letter, padded to the width, without taking an argument. The guest's `wcrtomb` and locales are not used. On Linux the formats without those conversions go to glibc, which prints NaN the same way. The `_s` forms throw for an unknown conversion, `%m` included, which was not measured -- [%a and %A](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) on Windows format a finite double as the `libc.prx` that titles ship does, instead of through the host: a subnormal is normalised (`0x1p-1074`), a shorter precision rounds half up on the dropped hexadecimal digits and a carry stays in the leading digit (`%.3a` of `0x0.fffffffffffffp-1022` is `0x2.000p-1023`), and `-` overrides `0`. Checked by running the copies of PPSA13934, PPSA07923 and PPSA05916 side by side on 7 million random formats, not measured on a console. Infinities and `long double` still go to the host; NaN prints without its sign, as in FreeBSD, where those copies print `-nan`. Linux hands `%a` to glibc, which prints a subnormal unnormalised +- [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) follows a payload process on a PS5 (firmware 13.60): each 16-bit unit of a `%ls` or `%lc` argument is encoded alone, so a surrogate pair gives two 3-byte sequences; a NaN keeps its sign and the `+` and space flags, and `0` pads it with spaces; argument positions above 99 work (up to 400 measured; above 65535 throw here); an unknown conversion such as `%m` or `%k` prints `%` and its letter, padded to the width, without taking an argument. The guest's `wcrtomb` and locales are not used. On Linux the formats without those conversions or `%a`/`%A` go to glibc, which prints NaN the same way. The `_s` forms throw for an unknown conversion, `%m` included, which was not measured +- [%a and %A](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) format a finite double as the `libc.prx` that titles ship does, instead of through the host: a subnormal is normalised (`0x1p-1074`), a shorter precision rounds half up on the dropped hexadecimal digits and a carry stays in the leading digit (`%.3a` of `0x0.fffffffffffffp-1022` is `0x2.000p-1023`), and `-` overrides `0`. Checked by running the copies of PPSA13934, PPSA07923 and PPSA05916 side by side on 7 million random formats, not measured on a console. Infinities and `long double` still go to the host; NaN keeps its sign and flags through the formatter on current main. - [%p and %lp](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) follow the console's `libSceLibcInternal.sprx` measured from a homebrew payload on firmware 12.70 ([#7549](https://github.com/boykopovar/AnyPS5/issues/7549)), not FreeBSD: bare lowercase hex with no `0x` prefix even under `#`, `0` for a null pointer, `+` and the space flag ignored, the width counting only the digits, `-` padding on the right, `0` padding with zeros when there is no precision, the precision as the minimum number of digits, nothing for a null pointer under `%.0p`, and `l` accepted. Only `snprintf` and `swprintf` of that module were measured, from a payload process rather than a title process, so a width beside `%.0p`, the `0` flag beside a precision (which C17 ignores) and `%.0p` of a non-null pointer follow this formatter's own rules and are not measured; `swprintf` was not run with `%lp`, so the wide family still refuses that conversion and returns -1. Linux routes `%p` and `%lp` to this formatter because glibc prints the FreeBSD form and `(nil)` for a null pointer, which means a format that combines `%p` with a conversion this formatter refuses, such as an argument position above 99, throws there instead of reaching glibc - [collate and collate _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build the facet for the C locale only and throw for any other locale name, the wide entry point following the narrow one it sits next to. The wide facet carries its own vtable, whose `compare`, `transform` and `hash` take the guest's 16-bit characters, so the length of a range is a count of characters: `compare` orders by 16-bit value (the C locale, as Dinkumware's `char_traits::compare` does), `transform` copies the characters into the result and records size and capacity in 16-bit elements (inline capacity 7, heap capacity `length | 7`, `(capacity + 1) * 2` bytes including the terminator), following Dinkumware's [VC12 `basic_string`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/xstring#L500-L531), and `hash` mixes one whole character per round, so wide text below 0x100 hashes like its narrow spelling and a character above 0xff hashes as one value instead of the two bytes it holds. The console's `libc.prx` exports both `_Getcat` (NIDs BSVJqITGCyI and 2wz4rthdiy8) with identical 0x19d-byte bodies, believed to build the same 0x20-byte facet; the console bodies themselves are unreadable (obfuscated dynsym, and the text of the relinked `libc.prx` is zeroed), so the wide entries follow the narrow algorithm and the C locale instead of the console's wide code, and no console run checks the two. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. The narrow result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware. No module of the PPSA59322, PPSA76677 or PPSA97358 dumps in this tree imports either `_Getcat`, so neither has been exercised by a title. - [codecvt, numpunct, num_put and time_put _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build facets matching a bundled console `libc.prx` (dynsym decoded by NID, vtables disassembled): categories 2, 4, 4 and 5, sizes 0x10, 0x30, 0x10 and 0xb8. `codecvt` reproduces the console virtuals (`noconv` conversions, `length` the shorter range); `numpunct` copies its C-locale table (`""`, `.`, `,`, `false`, `true`). `num_put`/`time_put` formatting virtuals throw rather than fabricate output, and `time_put`'s `_Locinfo` table is unpopulated — the console's `do_put` bodies were not mirrored. The facets share the no-op `destroy`/`deleteObject`/`retain`/`release` base used for `collate`, so `numpunct`'s static C-locale strings are never freed. Unlike `collate`, these `_Getcat` entries ignore the locale argument and always build the C-locale facet; the console's behaviour for a non-C name is unverified. `std::clog` and `std::wclog` are 0x400-byte zeroed objects like the rest of the `NSTDSTR` stream set (the console's `libc.prx` sizes `std::clog` at 96 bytes, measured from its dynsym `st_size`), and the `numpunct::id` and `time_put::id` objects are 8-byte zeros like the other facet ids. From 182bd6aa78842891f29e00bf113524775a6b897b Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 22 Oct 2026 16:42:46 -0400 Subject: [PATCH 253/428] fix(libc): round and cap %e, %f and %g as titles' libc.prx does The printf family formatted floating-point conversions through the host, which prints every exact digit and rounds ties to even. The libc.prx that titles ship keeps the exact digits only down to a position derived from the binary exponent and prints zeros after them, rounds half up on the kept digits, and rounds an exact tie down once that exponent estimate reaches 8. So %.0f of 2.5 is 3 and %.1f of 0.25 is 0.3 there, and %.20f of 0.1 is 0.10000000000000000555. Format finite double and long double e, f and g conversions with those rules from exactly generated digits, on every host. On 600,000 random formats every output with up to 17 significant digits now matches the copies bundled with PPSA07923, PPSA05916 and PPSA13934; longer outputs still differ where the console's extended-precision digits end one unit lower. Co-Authored-By: Claude Opus 5.5 --- .../prx/libc/include/WindowsFormatting.hpp | 243 ++++++++++++++++-- core/libs/prx/libc/src/Formatting.cpp | 2 +- core/libs/tests/Formatting.cpp | 76 ++++-- docs/dev/debt/libc.md | 3 +- 4 files changed, 286 insertions(+), 38 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index e92a088f39..2023452e25 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSFORMATTING_HPP #include "SceTypes.hpp" +#include #include #include #include @@ -161,33 +162,57 @@ inline void AppendUtf8(std::string& utf8, char32_t code) { } } -inline std::string HexFloat(const std::string& spec, char conversion, double value, std::size_t maximumLength = INT_MAX) { - bool left = false, plus = false, space = false, alternate = false, zero = false; +struct FloatSpec { + bool left = false; + bool plus = false; + bool space = false; + bool alternate = false; + bool zero = false; + std::size_t width = 0; + int precision = -1; +}; + +inline FloatSpec ParseFloatSpec(const std::string& spec) { + FloatSpec result; std::size_t index = 1; for (; index < spec.size() && std::strchr("-+ #0", spec[index]); ++index) { - left |= spec[index] == '-'; - plus |= spec[index] == '+'; - space |= spec[index] == ' '; - alternate |= spec[index] == '#'; - zero |= spec[index] == '0'; + result.left |= spec[index] == '-'; + result.plus |= spec[index] == '+'; + result.space |= spec[index] == ' '; + result.alternate |= spec[index] == '#'; + result.zero |= spec[index] == '0'; } - std::size_t width = 0; for (; index < spec.size() && spec[index] >= '0' && spec[index] <= '9'; ++index) { const auto digit = static_cast(spec[index] - '0'); - if (width > (static_cast(INT_MAX) - digit) / 10) + if (result.width > (static_cast(INT_MAX) - digit) / 10) throw std::overflow_error("Formatted output exceeds INT_MAX"); - width = width * 10 + digit; + result.width = result.width * 10 + digit; } - int precision = -1; if (index < spec.size() && spec[index] == '.') { - precision = 0; + result.precision = 0; for (++index; index < spec.size(); ++index) { const int digit = spec[index] - '0'; - if (precision > (INT_MAX - digit) / 10) + if (result.precision > (INT_MAX - digit) / 10) throw std::overflow_error("Floating precision exceeds int range"); - precision = precision * 10 + digit; + result.precision = result.precision * 10 + digit; } } + return result; +} + +inline std::string PadFloat(const FloatSpec& spec, const std::string& prefix, const std::string& body) { + const std::size_t length = prefix.size() + body.size(); + if (length >= spec.width) return prefix + body; + const std::string padding(spec.width - length, spec.left || !spec.zero ? ' ' : '0'); + if (spec.left) return prefix + body + padding; + return spec.zero ? prefix + padding + body : padding + prefix + body; +} + +inline std::string HexFloat(const std::string& spec, char conversion, double value, std::size_t maximumLength = INT_MAX) { + const FloatSpec parsed = ParseFloatSpec(spec); + const bool plus = parsed.plus, space = parsed.space, alternate = parsed.alternate; + const std::size_t width = parsed.width; + const int precision = parsed.precision; const bool upper = conversion == 'A'; const std::uint64_t bits = std::bit_cast(value); int exponent = static_cast((bits >> 52) & 0x7ff); @@ -234,10 +259,179 @@ inline std::string HexFloat(const std::string& spec, char conversion, double val for (int digit = digits - 1; digit >= 0; --digit) body += hex[(fraction >> (digit * 4)) & 0xf]; if (precision > 13) body.append(static_cast(precision - 13), '0'); body += suffix; - if (length >= width) return prefix + body; - const std::string padding(width - length, left || !zero ? ' ' : '0'); - if (left) return prefix + body + padding; - return zero ? prefix + padding + body : padding + prefix + body; + return PadFloat(parsed, prefix, body); +} + +struct DecimalDigits { + std::string digits; + long long exponent = 0; + bool truncated = false; + bool tiesDown = false; +}; + +inline std::string DecimalChunks(std::vector words) { + std::string text; + while (!words.empty()) { + std::uint64_t remainder = 0; + for (std::size_t index = words.size(); index-- > 0;) { + const std::uint64_t current = (remainder << 32) | words[index]; + words[index] = static_cast(current / 1000000000u); + remainder = current % 1000000000u; + } + while (!words.empty() && words.back() == 0) words.pop_back(); + std::string chunk = std::to_string(remainder); + if (!words.empty()) chunk.insert(0, 9 - chunk.size(), '0'); + text.insert(0, chunk); + } + return text; +} + +inline DecimalDigits ConsoleDecimalDigits(long double value) { + DecimalDigits result; + if (value == 0) return result; + int binary = 0; + const auto mantissa = static_cast(std::ldexp(std::frexp(std::fabs(value), &binary), 64)); + const long long estimate = static_cast(binary) * 30103 / 100000; + const long long last = (estimate >= 0 ? estimate / 4 : -((3 - estimate) / 4)) * 4 - 20; + result.tiesDown = estimate >= 8; + binary -= 64; + std::string integer; + std::string fraction; + bool remaining = false; + if (binary >= 0) { + const auto word = static_cast(binary) / 32; + const auto bit = static_cast(binary) % 32; + std::vector words(word + 3, 0); + for (std::size_t part = 0; part < 2; ++part) { + const std::uint64_t shifted = ((mantissa >> (32 * part)) & 0xffffffffu) << bit; + words[word + part] |= static_cast(shifted); + words[word + part + 1] |= static_cast(shifted >> 32); + } + while (!words.empty() && words.back() == 0) words.pop_back(); + integer = DecimalChunks(std::move(words)); + } else { + const auto shift = static_cast(-static_cast(binary)); + const std::uint64_t whole = shift >= 64 ? 0 : mantissa >> shift; + if (whole != 0) integer = std::to_string(whole); + std::vector words(shift / 32 + 2, 0); + const std::uint64_t remainder = shift >= 64 ? mantissa : mantissa & ((std::uint64_t{1} << shift) - 1); + words[0] = static_cast(remainder); + words[1] = static_cast(remainder >> 32); + const std::size_t top = shift / 32; + const auto bit = static_cast(shift % 32); + const auto significant = [&] { + const std::size_t leading = fraction.find_first_not_of('0'); + return integer.size() + (leading == std::string::npos ? 0 : fraction.size() - leading); + }; + remaining = remainder != 0; + while (remaining && significant() < 30) { + std::uint64_t carry = 0; + for (auto& word : words) { + const std::uint64_t product = static_cast(word) * 1000000000u + carry; + word = static_cast(product); + carry = product >> 32; + } + const std::uint64_t window = (static_cast(words[top + 1]) << 32) | words[top]; + const std::string chunk = std::to_string(static_cast(window >> bit)); + words[top + 1] = 0; + words[top] &= bit == 0 ? 0 : (std::uint32_t{1} << bit) - 1; + fraction += std::string(9 - chunk.size(), '0') + chunk; + remaining = std::any_of(words.begin(), words.end(), [](std::uint32_t word) { return word != 0; }); + } + } + if (!integer.empty()) { + result.exponent = static_cast(integer.size()) - 1; + result.digits = integer + fraction; + } else { + const std::size_t leading = fraction.find_first_not_of('0'); + result.exponent = -static_cast(leading) - 1; + result.digits = fraction.substr(leading); + } + const long long kept = result.exponent - last + 1; + if (static_cast(result.digits.size()) > kept) { + result.truncated = result.digits.find_first_not_of('0', static_cast(kept)) != std::string::npos; + result.digits.resize(static_cast(kept)); + } + result.truncated |= remaining; + return result; +} + +inline DecimalDigits RoundDecimal(const DecimalDigits& value, long long count) { + if (value.digits.empty()) return value; + if (count <= 0) { + if (count == 0 && value.digits[0] >= '5') return {"1", value.exponent + 1}; + return {}; + } + if (count >= static_cast(value.digits.size())) return value; + DecimalDigits result{value.digits.substr(0, static_cast(count)), value.exponent}; + const auto next = static_cast(count); + if (value.digits[next] < '5') return result; + if (value.digits[next] == '5' && value.tiesDown && !value.truncated && value.digits.find_first_not_of('0', next + 1) == std::string::npos) + return result; + for (std::size_t index = result.digits.size(); index-- > 0;) { + if (result.digits[index] != '9') { + ++result.digits[index]; + return result; + } + result.digits[index] = '0'; + } + result.digits.insert(result.digits.begin(), '1'); + result.digits.pop_back(); + ++result.exponent; + return result; +} + +inline char DigitAt(const DecimalDigits& value, long long position) { + const long long index = value.exponent - position; + if (value.digits.empty() || index < 0 || index >= static_cast(value.digits.size())) return '0'; + return value.digits[static_cast(index)]; +} + +inline std::string DecimalFloat(const std::string& spec, char conversion, long double value, std::size_t maximumLength = INT_MAX) { + const FloatSpec parsed = ParseFloatSpec(spec); + const DecimalDigits exact = ConsoleDecimalDigits(value); + const bool upper = conversion == 'E' || conversion == 'G'; + const char lower = static_cast(conversion | 0x20); + long long precision = parsed.precision < 0 ? 6 : parsed.precision; + bool exponential = lower == 'e'; + bool strip = false; + if (lower == 'g') { + if (precision == 0) precision = 1; + const long long exponent = exact.digits.empty() ? 0 : RoundDecimal(exact, precision).exponent; + exponential = !(precision > exponent && exponent >= -4); + precision = exponential ? precision - 1 : precision - 1 - exponent; + strip = !parsed.alternate; + } + const DecimalDigits rounded = RoundDecimal(exact, exponential ? precision + 1 : exact.exponent + 1 + precision); + const long long exponent = rounded.digits.empty() ? 0 : rounded.exponent; + if (strip) { + const long long last = rounded.digits.empty() ? 0 : rounded.exponent - static_cast(rounded.digits.size()) + 1; + long long needed = exponential ? exponent - last : -last; + while (needed > 0 && DigitAt(rounded, (exponential ? exponent : 0) - needed) == '0') --needed; + precision = std::max(0LL, std::min(precision, needed)); + } + const std::string prefix = std::signbit(value) ? "-" : parsed.plus ? "+" : parsed.space ? " " : ""; + const bool point = precision > 0 || parsed.alternate; + const std::string magnitude = std::to_string(exponent < 0 ? -exponent : exponent); + const long long whole = exponential || exponent < 0 ? 1 : exponent + 1; + const long long exponentLength = exponential ? 2 + static_cast(std::max(magnitude.size(), 2)) : 0; + const long long length = static_cast(prefix.size()) + whole + point + precision + exponentLength; + if (length > static_cast(maximumLength) || parsed.width > maximumLength) + throw std::overflow_error("Formatted output exceeds INT_MAX"); + std::string body; + body.reserve(static_cast(length)); + const long long first = exponential ? exponent : std::max(exponent, 0LL); + for (long long position = first; position >= (exponential ? exponent : 0); --position) body += DigitAt(rounded, position); + if (point) body += '.'; + const long long start = exponential ? exponent - 1 : -1; + for (long long position = start; position > start - precision; --position) body += DigitAt(rounded, position); + if (exponential) { + body += upper ? 'E' : 'e'; + body += exponent < 0 ? '-' : '+'; + if (magnitude.size() < 2) body += '0'; + body += magnitude; + } + return PadFloat(parsed, prefix, body); } inline std::string PaddedStringSpec(const std::string& spec) { @@ -453,8 +647,14 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo static_assert(sizeof(long double) == 16); static_assert(std::numeric_limits::digits == 64); const long double value = args.Next(); - if (std::isnan(value)) output.Value(PaddedStringSpec(spec), NanText(spec, std::signbit(value), conversion < 'a').c_str()); - else output.Value(spec + "L" + conversion, value); + if (std::isnan(value)) { + output.Value(PaddedStringSpec(spec), NanText(spec, std::signbit(value), conversion < 'a').c_str()); + } else if (conversion != 'a' && conversion != 'A' && std::isfinite(value)) { + const std::string text = DecimalFloat(spec, conversion, value, static_cast(INT_MAX - output.Count())); + output.Append(text.data(), text.size()); + } else { + output.Value(spec + "L" + conversion, value); + } } else { if (!length.empty() && length != "l") throw std::invalid_argument("Invalid floating length"); const double value = args.Next(); @@ -463,6 +663,9 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo } else if ((conversion == 'a' || conversion == 'A') && std::isfinite(value)) { const std::string text = HexFloat(spec, conversion, value, static_cast(INT_MAX - output.Count())); output.Append(text.data(), text.size()); + } else if (std::isfinite(value)) { + const std::string text = DecimalFloat(spec, conversion, value, static_cast(INT_MAX - output.Count())); + output.Append(text.data(), text.size()); } else { output.Value(spec + conversion, value); } diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 94d4896e1f..31cb245ee2 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -44,7 +44,7 @@ bool RequiresGuestFormatting(const char* format) { (*format == 'l' && (format[1] == 's' || format[1] == 'c' || format[1] == 'p'))) return true; while (*format && std::strchr("hljztL", *format)) ++format; if (*format && !std::strchr(LibcDetail::KnownConversions, *format)) return true; - if (*format && std::strchr("aA", *format)) return true; + if (*format && std::strchr("aAeEfFgG", *format)) return true; if (*format) ++format; } return false; diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index f40548444b..13b5d9016e 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -413,7 +413,7 @@ static void CheckAllocatingFormat() { free_nid_postfix(allocated); } -static void RequireHexFloat(const char* format, double value, const char* expected) { +static void RequireFloat(const char* format, double value, const char* expected) { char buffer[64]; const int written = snprintf_nid_postfix(buffer, sizeof(buffer), format, value); if (written != static_cast(std::strlen(expected)) || std::strcmp(buffer, expected) != 0) { @@ -423,20 +423,20 @@ static void RequireHexFloat(const char* format, double value, const char* expect } static void CheckHexFloat() { - RequireHexFloat("[%-010a]", 1.5, "[0x1.8p+0 ]"); - RequireHexFloat("[%-0#12A]", 0.75, "[0X1.8P-1 ]"); - RequireHexFloat("[%0-10.2a]", 1.0, "[0x1.00p+0 ]"); - RequireHexFloat("[%010a]", -1.5, "[-0x01.8p+0]"); - RequireHexFloat("[%a]", 0x1p-1074, "[0x1p-1074]"); - RequireHexFloat("[%a]", 0x0.fffffffffffffp-1022, "[0x1.ffffffffffffep-1023]"); - RequireHexFloat("[%.3a]", 0x0.fffffffffffffp-1022, "[0x2.000p-1023]"); - RequireHexFloat("[%A]", 0x0.012688b70e62bp-1022, "[0X1.2688B70E62BP-1030]"); - RequireHexFloat("[%.0a]", 0x0.012688b70e62bp-1022, "[0x1p-1030]"); - RequireHexFloat("[%.1a]", 0x1.08p0, "[0x1.1p+0]"); - RequireHexFloat("[%.0a]", 0x1.8p0, "[0x2p+0]"); - RequireHexFloat("[%#.0a]", 1.0, "[0x1.p+0]"); - RequireHexFloat("[%.20a]", 0.1, "[0x1.999999999999a0000000p-4]"); - RequireHexFloat("[% a]", -0.0, "[-0x0p+0]"); + RequireFloat("[%-010a]", 1.5, "[0x1.8p+0 ]"); + RequireFloat("[%-0#12A]", 0.75, "[0X1.8P-1 ]"); + RequireFloat("[%0-10.2a]", 1.0, "[0x1.00p+0 ]"); + RequireFloat("[%010a]", -1.5, "[-0x01.8p+0]"); + RequireFloat("[%a]", 0x1p-1074, "[0x1p-1074]"); + RequireFloat("[%a]", 0x0.fffffffffffffp-1022, "[0x1.ffffffffffffep-1023]"); + RequireFloat("[%.3a]", 0x0.fffffffffffffp-1022, "[0x2.000p-1023]"); + RequireFloat("[%A]", 0x0.012688b70e62bp-1022, "[0X1.2688B70E62BP-1030]"); + RequireFloat("[%.0a]", 0x0.012688b70e62bp-1022, "[0x1p-1030]"); + RequireFloat("[%.1a]", 0x1.08p0, "[0x1.1p+0]"); + RequireFloat("[%.0a]", 0x1.8p0, "[0x2p+0]"); + RequireFloat("[%#.0a]", 1.0, "[0x1.p+0]"); + RequireFloat("[%.20a]", 0.1, "[0x1.999999999999a0000000p-4]"); + RequireFloat("[% a]", -0.0, "[-0x0p+0]"); char buffer[32]; Require(snprintf_nid_postfix(buffer, sizeof(buffer), "[%*a]", -12, 1.5) == 14 && std::strcmp(buffer, "[0x1.8p+0 ]") == 0); Require(snprintf_nid_postfix(buffer, 6, "%a", 1.5) == 8 && std::strcmp(buffer, "0x1.8") == 0); @@ -456,4 +456,48 @@ static void CheckHexFloat() { #endif } -int main() { CheckSizeLimits(); RunChecks(); CheckHexFloat(); CheckAllocatingFormat(); return CheckWidePrecision() ? 0 : 1; } +static void CheckDecimalFloat() { + RequireFloat("[%.0f]", 0.5, "[1]"); + RequireFloat("[%.0f]", 2.5, "[3]"); + RequireFloat("[%.0f]", -0.5, "[-1]"); + RequireFloat("[%.1f]", 0.25, "[0.3]"); + RequireFloat("[%#.3g]", 2025.0, "[2.03e+03]"); + RequireFloat("[%.2e]", 1125.0, "[1.13e+03]"); + RequireFloat("[%.2f]", 0.015, "[0.01]"); + RequireFloat("[%.1f]", 99.95, "[100.0]"); + RequireFloat("[%.40e]", 0.1, "[1.0000000000000000555000000000000000000000e-01]"); + RequireFloat("[%.20f]", 0.1, "[0.10000000000000000555]"); + RequireFloat("[%.0f]", 0x1p100, "[1267650600228229401496700000000]"); + RequireFloat("[%.25e]", 0x1p-1074, "[4.9406564584124654417600000e-324]"); + RequireFloat("[%.23e]", 0x1.0f6b8p+1023, "[9.52987754335065669050000e+307]"); + RequireFloat("[%.17e]", -0x1.16a55a5cd7c73p+48, "[-3.06374418159559187e+14]"); + RequireFloat("[%.17g]", 0x1.e5dec6655f463p+50, "[2136879802973464.7]"); + RequireFloat("[%.0f]", 67108865.5, "[67108865]"); + RequireFloat("[%.0f]", 33554433.5, "[33554434]"); + RequireFloat("[%+08.1f]", 2.25, "[+00002.3]"); + RequireFloat("[%#.0e]", 2.5, "[3.e+00]"); + RequireFloat("[%g]", 0.0001, "[0.0001]"); + RequireFloat("[%G]", 0.00001, "[1E-05]"); + RequireFloat("[%.3g]", 999.5, "[1e+03]"); + RequireFloat("[%e]", 0.0, "[0.000000e+00]"); + RequireFloat("[%#g]", 0.0, "[0.00000]"); + RequireFloat("[%.0f]", -0.0, "[-0]"); + RequireFloat("[%-12.3E]", -1.25e-5, "[-1.250E-05 ]"); + char buffer[64]; + Require(snprintf_nid_postfix(buffer, sizeof(buffer), "[%.40Le]", 1.0L / 3) == 48 && std::strcmp(buffer, "[3.3333333333333333334000000000000000000000e-01]") == 0); + Require(snprintf_nid_postfix(buffer, sizeof(buffer), "[%.2Lf]", 0.125L) == 6 && std::strcmp(buffer, "[0.13]") == 0); + Require(snprintf_nid_postfix(buffer, sizeof(buffer), "[%.30Le]", 0x1p-16445L) == 40 && std::strcmp(buffer, "[3.645199531882474602528000000000e-4951]") == 0); + for (const char* format : {"%.2147483647e", "%+.2147483647E", "%.2147483647f", "% .2147483646F", "%#.2147483647g", + "%.2147483643e", "x%.2147483645f", "x%-2147483647e", "%.2147483647Le"}) { + bool rejected = false; + try { + if (std::strstr(format, "L")) snprintf_nid_postfix(buffer, sizeof(buffer), format, 1.5L); + else snprintf_nid_postfix(buffer, sizeof(buffer), format, 1.5); + } catch (const std::overflow_error&) { + rejected = true; + } + Require(rejected); + } +} + +int main() { CheckSizeLimits(); RunChecks(); CheckHexFloat(); CheckDecimalFloat(); CheckAllocatingFormat(); return CheckWidePrecision() ? 0 : 1; } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ee1b29fd58..23d8a0d941 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -135,8 +135,9 @@ - [wctomb_s, wcrtomb_s, mbstowcs_s and mbsrtowcs_s](../../../core/libs/prx/libc/src/BoundsChecking.cpp) (libc) follow the console's `libc.prx`, but convert in the single-byte C locale of `wctomb`, `wcrtomb` and `mbrtowc` (`MB_CUR_MAX` 1): a character above 0xFF is `EILSEQ` in `wctomb_s` and `wcrtomb_s`, and each byte is one wide character in `mbstowcs_s` and `mbsrtowcs_s`. The locale of a title's `libc.prx` was not measured. The `wcrtomb_s` path where a character does not fit `smax` and the `EILSEQ` paths of `mbstowcs_s` and `mbsrtowcs_s` are not reached - [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [c16rtomb, c32rtomb, mbrtoc16 and mbrtoc32](../../../core/libs/prx/libc/src/Multibyte.cpp) (libSceLibcInternal, implemented in libc) - a null state, which the console dereferences, throws. Otherwise they follow the console's `libSceLibcInternal.sprx` (0x7D480 to 0x7D920), read from disassembly: UTF-8 whatever the locale, up to six bytes per sequence, with no surrogate or 0x10FFFF check when encoding. The state holds the value in its first eight bytes and the expected byte count (0x10 for a pending low surrogate, 1 for a stored high one) as 16 bits at 0xA. `mbrtoc16` builds a low surrogate from the value the state last held, `c16rtomb` returns -1 without `errno` for an unpaired high surrogate, and `EILSEQ` is 86. On a PS5 (13.60) the system library's default locale converts UTF-8 in `wcrtomb`, `wctomb`, `mbrtowc` and `mblen`; a title's bundled `libc.prx` does too until `setlocale` installs a table ([disassembly](https://github.com/boykopovar/AnyPS5/pull/8855#issuecomment-6335267758)) -- [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) follows a payload process on a PS5 (firmware 13.60): each 16-bit unit of a `%ls` or `%lc` argument is encoded alone, so a surrogate pair gives two 3-byte sequences; a NaN keeps its sign and the `+` and space flags, and `0` pads it with spaces; argument positions above 99 work (up to 400 measured; above 65535 throw here); an unknown conversion such as `%m` or `%k` prints `%` and its letter, padded to the width, without taking an argument. The guest's `wcrtomb` and locales are not used. On Linux the formats without those conversions or `%a`/`%A` go to glibc, which prints NaN the same way. The `_s` forms throw for an unknown conversion, `%m` included, which was not measured +- [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) follows a payload process on a PS5 (firmware 13.60): each 16-bit unit of a `%ls` or `%lc` argument is encoded alone, so a surrogate pair gives two 3-byte sequences; a NaN keeps its sign and the `+` and space flags, and `0` pads it with spaces; argument positions above 99 work (up to 400 measured; above 65535 throw here); an unknown conversion such as `%m` or `%k` prints `%` and its letter, padded to the width, without taking an argument. The guest's `wcrtomb` and locales are not used. On Linux the formats without those conversions or a floating-point conversion go to glibc, which prints NaN the same way. The `_s` forms throw for an unknown conversion, `%m` included, which was not measured - [%a and %A](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) format a finite double as the `libc.prx` that titles ship does, instead of through the host: a subnormal is normalised (`0x1p-1074`), a shorter precision rounds half up on the dropped hexadecimal digits and a carry stays in the leading digit (`%.3a` of `0x0.fffffffffffffp-1022` is `0x2.000p-1023`), and `-` overrides `0`. Checked by running the copies of PPSA13934, PPSA07923 and PPSA05916 side by side on 7 million random formats, not measured on a console. Infinities and `long double` still go to the host; NaN keeps its sign and flags through the formatter on current main. +- [%e, %f and %g](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) format a finite `double` or `long double` as the `libc.prx` that titles ship does, instead of through the host. It keeps the exact decimal digits down to position `4 * floor(k / 4) - 20`, where `k` is the `frexp` exponent times 30103 / 100000 truncated, and prints zeros after them; a shorter precision rounds half up on the kept digits, except that an exact tie rounds down when `k` is at least 8 (`%.0f` of 67108865.5 is `67108865`). The console computes the kept digits in extended precision, so about 7% of outputs longer than 17 significant digits end one unit lower than here (`%f` of 1e22 is `9999999999999999999999.000000`). Checked by running the copies of PPSA07923, PPSA05916 and PPSA13934 side by side, not measured on a console. The wide printf family still formats through the host. - [%p and %lp](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) follow the console's `libSceLibcInternal.sprx` measured from a homebrew payload on firmware 12.70 ([#7549](https://github.com/boykopovar/AnyPS5/issues/7549)), not FreeBSD: bare lowercase hex with no `0x` prefix even under `#`, `0` for a null pointer, `+` and the space flag ignored, the width counting only the digits, `-` padding on the right, `0` padding with zeros when there is no precision, the precision as the minimum number of digits, nothing for a null pointer under `%.0p`, and `l` accepted. Only `snprintf` and `swprintf` of that module were measured, from a payload process rather than a title process, so a width beside `%.0p`, the `0` flag beside a precision (which C17 ignores) and `%.0p` of a non-null pointer follow this formatter's own rules and are not measured; `swprintf` was not run with `%lp`, so the wide family still refuses that conversion and returns -1. Linux routes `%p` and `%lp` to this formatter because glibc prints the FreeBSD form and `(nil)` for a null pointer, which means a format that combines `%p` with a conversion this formatter refuses, such as an argument position above 99, throws there instead of reaching glibc - [collate and collate _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build the facet for the C locale only and throw for any other locale name, the wide entry point following the narrow one it sits next to. The wide facet carries its own vtable, whose `compare`, `transform` and `hash` take the guest's 16-bit characters, so the length of a range is a count of characters: `compare` orders by 16-bit value (the C locale, as Dinkumware's `char_traits::compare` does), `transform` copies the characters into the result and records size and capacity in 16-bit elements (inline capacity 7, heap capacity `length | 7`, `(capacity + 1) * 2` bytes including the terminator), following Dinkumware's [VC12 `basic_string`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/xstring#L500-L531), and `hash` mixes one whole character per round, so wide text below 0x100 hashes like its narrow spelling and a character above 0xff hashes as one value instead of the two bytes it holds. The console's `libc.prx` exports both `_Getcat` (NIDs BSVJqITGCyI and 2wz4rthdiy8) with identical 0x19d-byte bodies, believed to build the same 0x20-byte facet; the console bodies themselves are unreadable (obfuscated dynsym, and the text of the relinked `libc.prx` is zeroed), so the wide entries follow the narrow algorithm and the C locale instead of the console's wide code, and no console run checks the two. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. The narrow result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware. No module of the PPSA59322, PPSA76677 or PPSA97358 dumps in this tree imports either `_Getcat`, so neither has been exercised by a title. - [codecvt, numpunct, num_put and time_put _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build facets matching a bundled console `libc.prx` (dynsym decoded by NID, vtables disassembled): categories 2, 4, 4 and 5, sizes 0x10, 0x30, 0x10 and 0xb8. `codecvt` reproduces the console virtuals (`noconv` conversions, `length` the shorter range); `numpunct` copies its C-locale table (`""`, `.`, `,`, `false`, `true`). `num_put`/`time_put` formatting virtuals throw rather than fabricate output, and `time_put`'s `_Locinfo` table is unpopulated — the console's `do_put` bodies were not mirrored. The facets share the no-op `destroy`/`deleteObject`/`retain`/`release` base used for `collate`, so `numpunct`'s static C-locale strings are never freed. Unlike `collate`, these `_Getcat` entries ignore the locale argument and always build the C-locale facet; the console's behaviour for a non-C name is unverified. `std::clog` and `std::wclog` are 0x400-byte zeroed objects like the rest of the `NSTDSTR` stream set (the console's `libc.prx` sizes `std::clog` at 96 bytes, measured from its dynsym `st_size`), and the `numpunct::id` and `time_put::id` objects are 8-byte zeros like the other facet ids. From 2ad29a5a91722a9fa48e08b4e3f214c0d837e479 Mon Sep 17 00:00:00 2001 From: Bram910 Date: Sat, 24 Oct 2026 22:17:44 +0200 Subject: [PATCH 254/428] fix(tools): match workflow test scripts only as whole paths Co-Authored-By: Claude Opus 5.5 --- tools/check_test_registration.py | 2 +- tools/tests/test_test_registration.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index e74e2f72e6..d29473a451 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -11,7 +11,7 @@ LEXICAL = re.compile(r'\\.|"(?:\\.|[^"\\])*"|\[(?P=*)\[.*?\](?P= r'|(?P\#\[(?P=*)\[.*?\](?P=comment_equals)\]|\#[^\n]*)', re.S) KNOWN_UNRUN = set() -SCRIPT = re.compile(r"(?:[\w.-]+/)*test_\w+\.py") +SCRIPT = re.compile(r"(? Date: Sat, 24 Oct 2026 22:24:03 +0200 Subject: [PATCH 255/428] fix(tools): report refused JobManager modules cleanly --- tools/run_job_manager_reference.py | 4 +- tools/tests/test_unwrap_self.py | 59 ++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/tools/run_job_manager_reference.py b/tools/run_job_manager_reference.py index d804f3e573..7f04d25637 100644 --- a/tools/run_job_manager_reference.py +++ b/tools/run_job_manager_reference.py @@ -9,7 +9,7 @@ import tempfile from pathlib import Path from import_audit import read_host_imports -from unwrap_self import unwrap +from unwrap_self import Refused, unwrap ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "core/relinker/relinker/tests")) @@ -124,7 +124,7 @@ def main(): directory.mkdir(parents=True, exist_ok=False) print(f"Artifacts: {directory}", flush=True) return run(args, directory) - except (OSError, RuntimeError, subprocess.SubprocessError) as error: + except (OSError, Refused, RuntimeError, subprocess.SubprocessError) as error: print(str(error), file=sys.stderr) if isinstance(error, subprocess.CalledProcessError) and error.stderr: print(error.stderr.decode(errors="replace"), file=sys.stderr) diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index 035a38e71f..1d695d2a4b 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -1041,5 +1041,64 @@ class UnwrapTest(unittest.TestCase): self.assertEqual(err.errors, "backslashreplace") +class JobManagerModuleRefusalTests(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + self.build = self.root / "build" + self.helper = self.build / "tests" / "job_manager_reference.prx" + self.helper.parent.mkdir(parents=True) + self.helper.write_bytes(b"reference helper") + self.relinker = self.root / "relinker.exe" + self.relinker.touch() + self.toolchain = self.root / "toolchain" + self.toolchain.mkdir() + self.module = self.root / "module.self" + + def invoke(self, output): + import run_job_manager_reference + arguments = ["run_job_manager_reference.py", "--module", str(self.module), + "--build", str(self.build), "--relinker", str(self.relinker), + "--toolchain", str(self.toolchain), "--output", str(output)] + stdout, stderr = io.StringIO(), io.StringIO() + with mock.patch.object(sys, "argv", arguments), mock.patch.object(sys, "platform", "win32"), \ + mock.patch.object(run_job_manager_reference.subprocess, "run") as process, \ + contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): + code = run_job_manager_reference.main() + return code, stdout.getvalue(), stderr.getvalue(), process + + def test_module_refusals_stop_reference_setup_with_a_diagnostic(self): + malformed_elf = bytearray(build_container(default_segments(), default_records(), default_payloads())) + malformed_elf[ELF_HEADER_OFFSET + 4] = 1 + cases = ((b"short", "file is smaller than the SELF header area"), + (bytes(FIRST_REGION), "not a SELF container (magic 0x0)"), + (bytes(malformed_elf), "inner ELF is not 64-bit little-endian")) + for index, (data, diagnostic) in enumerate(cases): + with self.subTest(diagnostic=diagnostic): + self.module.write_bytes(data) + output = self.root / f"artifacts-{index}" + code, stdout, stderr, process = self.invoke(output) + self.assertEqual(code, 2) + self.assertEqual(stderr, diagnostic + "\n") + self.assertIn(f"Artifacts: {output}", stdout) + self.assertNotIn("Traceback", stderr) + self.assertEqual(self.module.read_bytes(), data) + self.assertEqual((output / "libs" / "jobref.prx").read_bytes(), b"reference helper") + self.assertFalse((output / "sce_module" / "libSceJobManager.prx").exists()) + self.assertFalse((output / "relink.log").exists()) + self.assertFalse((output / "results.json").exists()) + process.assert_called_once() + self.assertEqual(process.call_args.args[0][0], str(self.build / "core/libs/nid_patcher.exe")) + + def test_missing_reference_helper_keeps_existing_error_handling(self): + self.helper.unlink() + self.module.write_bytes(b"short") + code, stdout, stderr, process = self.invoke(self.root / "artifacts") + self.assertEqual(code, 2) + self.assertEqual(stderr, "Build the job_manager_reference target first\n") + process.assert_not_called() + + if __name__ == "__main__": unittest.main() From 372a31ed41c15e1124282af228df7a93ac940061 Mon Sep 17 00:00:00 2001 From: Kyon Date: Sat, 24 Oct 2026 20:54:54 +0200 Subject: [PATCH 256/428] feat(libSceSulphaDrv): implement default configuration --- core/libs/prx/libSceSulphaDrv/CMakeLists.txt | 15 ++++++ core/libs/prx/libSceSulphaDrv/Export.cpp | 7 ++- core/libs/tests/GuestSulphaConfig.cpp | 48 ++++++++++++++++++++ docs/dev/debt/libSceSulphaDrv.md | 4 +- 4 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 core/libs/prx/libSceSulphaDrv/CMakeLists.txt create mode 100644 core/libs/tests/GuestSulphaConfig.cpp diff --git a/core/libs/prx/libSceSulphaDrv/CMakeLists.txt b/core/libs/prx/libSceSulphaDrv/CMakeLists.txt new file mode 100644 index 0000000000..649d75c4d0 --- /dev/null +++ b/core/libs/prx/libSceSulphaDrv/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceSulphaDrv SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceSulphaDrv PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceSulphaDrv) + +if(BUILD_TESTING) + add_test_executable(guest_sulpha_config_tests ${LIBS_INCLUDE_DIR}/tests/GuestSulphaConfig.cpp) + target_include_directories(guest_sulpha_config_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sulpha_config_tests PRIVATE libSceSulphaDrv libc) + configure_windows_unwind(guest_sulpha_config_tests) + add_test(NAME guest_sulpha_config COMMAND guest_sulpha_config_tests) + if(WIN32) + set_tests_properties(guest_sulpha_config PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$;PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceSulphaDrv/Export.cpp b/core/libs/prx/libSceSulphaDrv/Export.cpp index b956051655..a950de35ba 100644 --- a/core/libs/prx/libSceSulphaDrv/Export.cpp +++ b/core/libs/prx/libSceSulphaDrv/Export.cpp @@ -1,5 +1,6 @@ #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -16,8 +17,10 @@ int APS5_VABI sceSulphaGetConnectedClientCount(void) { return 0; } -int APS5_VABI sceSulphaGetDefaultConfig(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceSulphaGetDefaultConfig(void* config) { + if (config == nullptr) return static_cast(0x804D0003u); + static constexpr std::uint64_t defaults[]{24, 0x400000, 0xFA100000003}; + std::memcpy(config, defaults, sizeof(defaults)); return 0; } diff --git a/core/libs/tests/GuestSulphaConfig.cpp b/core/libs/tests/GuestSulphaConfig.cpp new file mode 100644 index 0000000000..178a80f6d9 --- /dev/null +++ b/core/libs/tests/GuestSulphaConfig.cpp @@ -0,0 +1,48 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI sceSulphaGetDefaultConfig(void* config); + +static void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +int main() { + errno = 7; + Require(sceSulphaGetDefaultConfig(nullptr) == static_cast(0x804d0003u) && errno == 7, + "A null configuration must return the native argument error without changing errno"); + struct Config { + std::uint64_t size; + std::uint64_t capacity; + std::uint32_t setting; + std::uint32_t port; + }; + static_assert(sizeof(Config) == 24); + for (const auto shift : {0u, 1u}) for (const auto initial : std::array{0, 24, 560, UINT64_MAX}) { + for (const unsigned char pattern : {0xa5, 0x5a}) { + std::array storage; + storage.fill(pattern); + auto* config = storage.data() + 8 + shift; + std::memcpy(config, &initial, sizeof(initial)); + errno = 7; + Require(sceSulphaGetDefaultConfig(config) == 0 && errno == 7, + "Default configuration must accept non-null storage regardless of its incoming size"); + Config result; + std::memcpy(&result, config, sizeof(result)); + Require(result.size == 24 && result.capacity == 4194304 && result.setting == 3 && result.port == 4001, + "Default configuration must match the native 24-byte record"); + for (std::size_t i = 0; i < storage.size(); ++i) { + if (i < 8 + shift || i >= 8 + shift + sizeof(Config)) + Require(storage[i] == pattern, "Default configuration must preserve bytes outside its record"); + } + } + } +} diff --git a/docs/dev/debt/libSceSulphaDrv.md b/docs/dev/debt/libSceSulphaDrv.md index 88b6f9091f..990eaa1951 100644 --- a/docs/dev/debt/libSceSulphaDrv.md +++ b/docs/dev/debt/libSceSulphaDrv.md @@ -1,5 +1,5 @@ # libSceSulphaDrv technical debt -### Inventory exports declared elsewhere +### Unknown function info -sceSulphaGetClientInfo, sceSulphaGetConnectedClientCount, sceSulphaGetDefaultConfig, sceSulphaGetNeededMemory, sceSulphaInit, sceSulphaMessage, sceSulphaSetBookmark and sceSulphaShutdown are the complete PS5 3.20 export table of this module per the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSulphaDrv.c), but all eight are currently declared by #7416 in libSceSulpha, so this module keeps its `APS5_DUMMY_FUN` skeleton to respect the duplicate-export rule. Declare them here (with no parameters, matching the `1b5004d77` unknown-prototype shape) if that PR's module attribution is rejected. +- [libSceSulphaDrv](../../../core/libs/prx/libSceSulphaDrv/Export.cpp) - signatures and behavior remain unknown for `sceSulphaGetClientInfo`, `sceSulphaGetConnectedClientCount`, `sceSulphaGetNeededMemory`, `sceSulphaInit`, `sceSulphaMessage`, `sceSulphaSetBookmark` and `sceSulphaShutdown`. Their no-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch`; they make no ABI claim. From dfd99b7cb5fcfcf0b1e3275fa888e39a9450acb3 Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Sat, 24 Oct 2026 22:11:04 +0200 Subject: [PATCH 257/428] fix(tools): name a missing head instead of reporting no common ancestor in check_main_sync report() turned every nonzero exit of git merge-base into " and have no common ancestor", so a mistyped or unfetched --head, a tree or a blob was reported as unrelated history and git's own error was dropped. Now only exit status 1 keeps that message, and any other failure raises "git merge-base failed: " like the merge-tree call in conflicts(). git merge-base returns 1 when the histories share no commit and dies with 128 when a revision does not resolve to a commit. The tool still exits 2 in both cases, and test_unrelated_histories_are_an_error now also checks that an invalid head reports git's own merge-base error and not a missing common ancestor. --- tools/check_main_sync.py | 4 +++- tools/tests/test_check_main_sync.py | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/tools/check_main_sync.py b/tools/check_main_sync.py index f6d7532082..0fdfbdfc79 100644 --- a/tools/check_main_sync.py +++ b/tools/check_main_sync.py @@ -54,8 +54,10 @@ def merged_pull_requests(repo, mergebase, base, paths): def report(repo, base, head): repo = git(repo, "rev-parse", "--show-toplevel").stdout.strip() found = git(repo, "merge-base", base, head, check=False) - if found.returncode: + if found.returncode == 1: raise RuntimeError(f"{head} and {base} have no common ancestor") + if found.returncode: + raise RuntimeError(f"git merge-base failed: {found.stderr.strip()}") mergebase = found.stdout.strip() behind = int(git(repo, "rev-list", "--count", f"{head}..{base}").stdout) ahead = int(git(repo, "rev-list", "--count", f"{base}..{head}").stdout) diff --git a/tools/tests/test_check_main_sync.py b/tools/tests/test_check_main_sync.py index a82463563e..15d1e5c287 100644 --- a/tools/tests/test_check_main_sync.py +++ b/tools/tests/test_check_main_sync.py @@ -166,6 +166,15 @@ class CheckMainSyncTests(unittest.TestCase): result = run_tool(self.repo.path, "--head", "island") self.assertEqual(result.returncode, 2) self.assertIn("no common ancestor", result.stderr) + result = run_tool(self.repo.path, "--head", "missing") + expected = subprocess.run( + ["git", "-C", str(self.repo.path), "merge-base", "main", "missing"], + capture_output=True, + encoding="utf-8", + ).stderr.strip() + self.assertEqual(result.returncode, 2) + self.assertIn(f"git merge-base failed: {expected}", result.stderr) + self.assertNotIn("no common ancestor", result.stderr) def check_unicode_output(self, conflict): name = "sp ace \u00e9 \u6e2c \U0001f680.txt" From 856885465ddda631978839ef38529a960e33b750 Mon Sep 17 00:00:00 2001 From: Kyon Date: Sat, 24 Oct 2026 22:30:02 +0200 Subject: [PATCH 258/428] fix(libSceSulphaDrv): share the canonical configuration declaration --- core/libs/prx/libSceSulphaDrv/Export.cpp | 1 + core/libs/prx/libSceSulphaDrv/include/Api.hpp | 16 ++++++++++++++++ .../libs/prx/libSceSulphaDrv/libSceSulphaDrv.hpp | 3 +++ core/libs/tests/GuestSulphaConfig.cpp | 4 +--- 4 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceSulphaDrv/include/Api.hpp create mode 100644 core/libs/prx/libSceSulphaDrv/libSceSulphaDrv.hpp diff --git a/core/libs/prx/libSceSulphaDrv/Export.cpp b/core/libs/prx/libSceSulphaDrv/Export.cpp index a950de35ba..a40021ac2d 100644 --- a/core/libs/prx/libSceSulphaDrv/Export.cpp +++ b/core/libs/prx/libSceSulphaDrv/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceSulphaDrv/include/Api.hpp" #include #include #include diff --git a/core/libs/prx/libSceSulphaDrv/include/Api.hpp b/core/libs/prx/libSceSulphaDrv/include/Api.hpp new file mode 100644 index 0000000000..86b1f74a4a --- /dev/null +++ b/core/libs/prx/libSceSulphaDrv/include/Api.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceSulphaGetClientInfo(void); +int APS5_VABI sceSulphaGetConnectedClientCount(void); +int APS5_VABI sceSulphaGetDefaultConfig(void* config); +int APS5_VABI sceSulphaGetNeededMemory(void); +int APS5_VABI sceSulphaInit(void); +int APS5_VABI sceSulphaMessage(void); +int APS5_VABI sceSulphaSetBookmark(void); +int APS5_VABI sceSulphaShutdown(void); + +} diff --git a/core/libs/prx/libSceSulphaDrv/libSceSulphaDrv.hpp b/core/libs/prx/libSceSulphaDrv/libSceSulphaDrv.hpp new file mode 100644 index 0000000000..1f85062b91 --- /dev/null +++ b/core/libs/prx/libSceSulphaDrv/libSceSulphaDrv.hpp @@ -0,0 +1,3 @@ +#pragma once + +#include "prx/libSceSulphaDrv/include/Api.hpp" diff --git a/core/libs/tests/GuestSulphaConfig.cpp b/core/libs/tests/GuestSulphaConfig.cpp index 178a80f6d9..007930e68a 100644 --- a/core/libs/tests/GuestSulphaConfig.cpp +++ b/core/libs/tests/GuestSulphaConfig.cpp @@ -1,4 +1,4 @@ -#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceSulphaDrv/libSceSulphaDrv.hpp" #include #include #include @@ -7,8 +7,6 @@ #include #include -extern "C" int APS5_VABI sceSulphaGetDefaultConfig(void* config); - static void Require(bool condition, const char* message) { if (condition) return; std::fprintf(stderr, "%s\n", message); From 17eb2ea5f8101fa166d5d5490116cb043712e967 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Wed, 21 Oct 2026 03:33:29 +0200 Subject: [PATCH 259/428] fix(agc): retire an undrawn filled multisampled color target that a guest access overlaps --- .../Graphics/src/MultisampleTarget.cpp | 52 ++++++++++++++----- .../execution/SampleMultisampleTarget.cpp | 14 +++++ 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 0520466262..372da695b3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -500,6 +500,7 @@ public: Context context; std::shared_ptr> active = std::make_shared>(true); + std::shared_ptr> drawn = std::make_shared>(true); std::shared_ptr> metadataValid; ColorTarget target; VkImage image = VK_NULL_HANDLE; @@ -660,6 +661,7 @@ struct GuardedRange { std::uint32_t samples; std::shared_ptr> active; std::shared_ptr> metadataValid; + std::shared_ptr> drawn; bool fmask = false; }; @@ -734,7 +736,7 @@ void retireUnmapped(std::uintptr_t address, std::size_t bytes) { anyGuarded().store(!guarded().empty(), std::memory_order_release); } -void guard(const Context& context, const ColorTarget& target, const std::shared_ptr>& active, const std::shared_ptr>& metadataValid) { +void guard(const Context& context, const ColorTarget& target, const std::shared_ptr>& active, const std::shared_ptr>& metadataValid, const std::shared_ptr>& drawn) { auto bytes = std::max(target.bytes * target.samples, static_cast(target.extent.width) * target.extent.height * target.elementBytes * target.samples); if (MultisampledBlockLog2(target.tileMode) == 16u && target.tileMode != ColorTileMode::Standard64KB && std::has_single_bit(target.elementBytes) && std::has_single_bit(target.samples)) { const auto log2Samples = std::countr_zero(target.samples); @@ -746,11 +748,11 @@ void guard(const Context& context, const ColorTarget& target, const std::shared_ bytes = align(target.extent.width, blockWidth) * align(target.extent.height, blockHeight) * target.elementBytes * target.samples; } std::unique_lock lock(guardedMutex()); - guarded().push_back({context.device, target.address, bytes, target.samples, active}); + guarded().push_back({context.device, target.address, bytes, target.samples, active, nullptr, drawn}); if (target.dccAddress != 0) { - guarded().push_back({context.device, target.dccAddress, MultisampledDccBytes(target), target.samples, active, metadataValid}); - if (target.cmaskAddress != 0) guarded().push_back({context.device, target.cmaskAddress, target.cmaskBytes, target.samples, active, metadataValid}); - if (target.samples == 8u && target.fmaskAddress != 0u && target.cmaskAddress != 0u) guarded().push_back({context.device, target.fmaskAddress, DescribeSurface(fmaskResource(target)).guestBytes, target.samples, active, metadataValid}); + guarded().push_back({context.device, target.dccAddress, MultisampledDccBytes(target), target.samples, active, metadataValid, drawn}); + if (target.cmaskAddress != 0) guarded().push_back({context.device, target.cmaskAddress, target.cmaskBytes, target.samples, active, metadataValid, drawn}); + if (target.samples == 8u && target.fmaskAddress != 0u && target.cmaskAddress != 0u) guarded().push_back({context.device, target.fmaskAddress, DescribeSurface(fmaskResource(target)).guestBytes, target.samples, active, metadataValid, drawn}); } anyGuarded().store(true, std::memory_order_release); InstallResidentImageReadGuard_nid_no_patch(); @@ -784,10 +786,16 @@ MultisampleTarget& acquire(const Context& context, const ColorTarget& requested) anyGuarded().store(!guarded().empty(), std::memory_order_release); } target->Recreate(use); - guard(context, use, target->active, target->metadataValid); + target->drawn->store(true, std::memory_order_release); + guard(context, use, target->active, target->metadataValid, target->drawn); target->ApplyFastClear(use); return *target; } + { + std::unique_lock lock(guardedMutex()); + if (!target->active->load(std::memory_order_acquire)) continue; + target->drawn->store(true, std::memory_order_release); + } if (target->fromTexture && use.cmaskAddress != 0) { target->fromTexture = false; target->cmask = MultisampledCmask(use.cmaskAddress, use.cmaskBytes); @@ -803,7 +811,7 @@ MultisampleTarget& acquire(const Context& context, const ColorTarget& requested) target->Keep(); targets().push_back(std::move(target)); auto& created = *targets().back(); - guard(context, use, created.active, created.metadataValid); + guard(context, use, created.active, created.metadataValid, created.drawn); if (use.cmaskFastClear && use.dccAddress == 0) created.cmask.SeedCode(CurrentUniformMetadataByte(use.cmaskAddress, static_cast(use.cmaskBytes))); created.ApplyFastClear(use); created.Keep(); @@ -877,7 +885,8 @@ MultisampleTarget& createFilled(const Context& context, const GuestTextureResour target->Keep(); targets().push_back(std::move(target)); auto& created = *targets().back(); - guard(context, use, created.active, created.metadataValid); + created.drawn->store(false, std::memory_order_release); + guard(context, use, created.active, created.metadataValid, created.drawn); created.colorFill.NoteFill(use.address, static_cast(bytes), value); created.fromTexture = true; created.htileAddress = resource.dccAddress; @@ -1327,7 +1336,7 @@ void GuardOneFragmentFmask(const Context& context, const ColorTarget& target) { const auto bytes = std::max((bits / 8u + 0xffffu) & ~0xffffull, 0x10000u); std::unique_lock lock(guardedMutex()); const bool known = std::ranges::any_of(guarded(), [&](const auto& range) { return range.fmask && range.device == context.device && range.address == target.fmaskAddress && range.bytes == bytes; }); - if (!known) guarded().push_back({context.device, target.fmaskAddress, bytes, target.coverageSamples, std::make_shared>(true), nullptr, true}); + if (!known) guarded().push_back({context.device, target.fmaskAddress, bytes, target.coverageSamples, std::make_shared>(true), nullptr, std::make_shared>(true), true}); anyGuarded().store(true, std::memory_order_release); lock.unlock(); InstallResidentImageReadGuard_nid_no_patch(); @@ -1340,9 +1349,8 @@ void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const void RequireNoMultisampleColorTarget(std::uint64_t address, std::size_t bytes, const char* access) { if (bytes == 0 || !anyGuarded().load(std::memory_order_acquire)) return; - std::shared_lock lock(guardedMutex()); - for (const auto& range : guarded()) { - if (address >= range.address + range.bytes || range.address >= address + bytes) continue; + const auto overlaps = [&](const GuardedRange& range) { return address < range.address + range.bytes && range.address < address + bytes; }; + const auto refuse = [&](const GuardedRange& range) { char text[256]; if (range.fmask) { std::snprintf(text, sizeof(text), "AGC graphics: %s of 0x%llx+0x%zx overlaps the FMASK 0x%llx+0x%llx of a one-fragment color target of %u samples drawn on the host, which does not write it: guest access to it is not implemented", access, static_cast(address), bytes, static_cast(range.address), static_cast(range.bytes), range.samples); @@ -1350,7 +1358,27 @@ void RequireNoMultisampleColorTarget(std::uint64_t address, std::size_t bytes, c } std::snprintf(text, sizeof(text), "AGC graphics: %s of 0x%llx+0x%zx overlaps multisampled color target 0x%llx+0x%llx (%u samples), whose samples are only on the host: guest access to it is not implemented", access, static_cast(address), bytes, static_cast(range.address), static_cast(range.bytes), range.samples); throw std::runtime_error(text); + }; + { + std::shared_lock lock(guardedMutex()); + bool undrawn = false; + for (const auto& range : guarded()) { + if (!overlaps(range)) continue; + if (range.drawn->load(std::memory_order_acquire)) refuse(range); + undrawn = true; + } + if (!undrawn) return; } + std::unique_lock lock(guardedMutex()); + std::vector>> retired; + for (const auto& range : guarded()) { + if (!overlaps(range)) continue; + if (range.drawn->load(std::memory_order_acquire)) refuse(range); + retired.push_back(range.active); + } + for (const auto& active : retired) active->store(false, std::memory_order_release); + std::erase_if(guarded(), [&](const auto& range) { return std::find(retired.begin(), retired.end(), range.active) != retired.end(); }); + anyGuarded().store(!guarded().empty(), std::memory_order_release); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index 69392bd173..d679f6446f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -1015,6 +1015,20 @@ int main(int argc, char** argv) { Block stored(BlockBytes); StoreWords(stored, colorBytes, FillOnes); CheckUniform(LoadSamples(*device, stored), Ones, "from color memory of half-float ones no draw rendered to"); + Block reused(BlockBytes); + StoreWords(reused, colorBytes, FillOnes); + CheckUniform(LoadSamples(*device, reused), Ones, "from color memory of half-float ones no draw rendered to, before the title reuses it"); + AgcDriver::Graphics::RequireNoMultisampleColorTarget(reused.Address(), static_cast(colorBytes / 2u), "a storage image access"); + StoreWords(reused, colorBytes, 0u); + CheckUniform(LoadSamples(*device, reused), {}, "from color memory no draw rendered to after the title reused it for zeros"); + std::string drawnRefusal; + try { + AgcDriver::Graphics::RequireNoMultisampleColorTarget(block.Address(), static_cast(colorBytes / 2u), "a storage image access"); + } catch (const std::exception& error) { + drawnRefusal = error.what(); + } + Require(drawnRefusal.find("whose samples are only on the host") != std::string::npos, "a guest access over a drawn 4x target is not refused"); + std::puts("a guest access over a 4x texture no draw rendered to retires it, so a later load reads the memory's new value, and a guest access over a drawn target is still refused"); const auto storedState = TargetState(DecodeTarget(stored)); Draw(*device, storedState); CheckSamples(storedState, LoadSamples(*device, stored), Ones); From dcd88603ca4485273d58f2ce038760a166a27804 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Wed, 21 Oct 2026 03:40:45 +0200 Subject: [PATCH 260/428] fix(agc): keep an undrawn multisampled target a dispatch wrote or a fill cleared guarded --- .../Graphics/src/MultisampleTarget.cpp | 64 +++++++++++-------- .../execution/SampleMultisampleTarget.cpp | 17 +++++ 2 files changed, 56 insertions(+), 25 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 372da695b3..ff57768169 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -500,7 +500,7 @@ public: Context context; std::shared_ptr> active = std::make_shared>(true); - std::shared_ptr> drawn = std::make_shared>(true); + std::shared_ptr> hostOnly = std::make_shared>(true); std::shared_ptr> metadataValid; ColorTarget target; VkImage image = VK_NULL_HANDLE; @@ -661,7 +661,7 @@ struct GuardedRange { std::uint32_t samples; std::shared_ptr> active; std::shared_ptr> metadataValid; - std::shared_ptr> drawn; + std::shared_ptr> hostOnly; bool fmask = false; }; @@ -736,7 +736,7 @@ void retireUnmapped(std::uintptr_t address, std::size_t bytes) { anyGuarded().store(!guarded().empty(), std::memory_order_release); } -void guard(const Context& context, const ColorTarget& target, const std::shared_ptr>& active, const std::shared_ptr>& metadataValid, const std::shared_ptr>& drawn) { +void guard(const Context& context, const ColorTarget& target, const std::shared_ptr>& active, const std::shared_ptr>& metadataValid, const std::shared_ptr>& hostOnly) { auto bytes = std::max(target.bytes * target.samples, static_cast(target.extent.width) * target.extent.height * target.elementBytes * target.samples); if (MultisampledBlockLog2(target.tileMode) == 16u && target.tileMode != ColorTileMode::Standard64KB && std::has_single_bit(target.elementBytes) && std::has_single_bit(target.samples)) { const auto log2Samples = std::countr_zero(target.samples); @@ -748,11 +748,11 @@ void guard(const Context& context, const ColorTarget& target, const std::shared_ bytes = align(target.extent.width, blockWidth) * align(target.extent.height, blockHeight) * target.elementBytes * target.samples; } std::unique_lock lock(guardedMutex()); - guarded().push_back({context.device, target.address, bytes, target.samples, active, nullptr, drawn}); + guarded().push_back({context.device, target.address, bytes, target.samples, active, nullptr, hostOnly}); if (target.dccAddress != 0) { - guarded().push_back({context.device, target.dccAddress, MultisampledDccBytes(target), target.samples, active, metadataValid, drawn}); - if (target.cmaskAddress != 0) guarded().push_back({context.device, target.cmaskAddress, target.cmaskBytes, target.samples, active, metadataValid, drawn}); - if (target.samples == 8u && target.fmaskAddress != 0u && target.cmaskAddress != 0u) guarded().push_back({context.device, target.fmaskAddress, DescribeSurface(fmaskResource(target)).guestBytes, target.samples, active, metadataValid, drawn}); + guarded().push_back({context.device, target.dccAddress, MultisampledDccBytes(target), target.samples, active, metadataValid, hostOnly}); + if (target.cmaskAddress != 0) guarded().push_back({context.device, target.cmaskAddress, target.cmaskBytes, target.samples, active, metadataValid, hostOnly}); + if (target.samples == 8u && target.fmaskAddress != 0u && target.cmaskAddress != 0u) guarded().push_back({context.device, target.fmaskAddress, DescribeSurface(fmaskResource(target)).guestBytes, target.samples, active, metadataValid, hostOnly}); } anyGuarded().store(true, std::memory_order_release); InstallResidentImageReadGuard_nid_no_patch(); @@ -772,6 +772,13 @@ void discardRetiredTargets() { std::erase_if(targets(), [](const auto& target) { return !target->active->load(std::memory_order_acquire); }); } +bool holdHostOnly(const MultisampleTarget& target) { + std::unique_lock lock(guardedMutex()); + if (!target.active->load(std::memory_order_acquire)) return false; + target.hostOnly->store(true, std::memory_order_release); + return true; +} + MultisampleTarget& acquire(const Context& context, const ColorTarget& requested) { discardRetiredTargets(); for (const auto& target : targets()) { @@ -782,19 +789,20 @@ MultisampleTarget& acquire(const Context& context, const ColorTarget& requested) if (recreated(target->target, use)) { { std::unique_lock lock(guardedMutex()); + if (!target->active->load(std::memory_order_acquire)) continue; std::erase_if(guarded(), [&](const auto& range) { return range.active == target->active; }); anyGuarded().store(!guarded().empty(), std::memory_order_release); } target->Recreate(use); - target->drawn->store(true, std::memory_order_release); - guard(context, use, target->active, target->metadataValid, target->drawn); + target->hostOnly->store(true, std::memory_order_release); + guard(context, use, target->active, target->metadataValid, target->hostOnly); target->ApplyFastClear(use); return *target; } { std::unique_lock lock(guardedMutex()); if (!target->active->load(std::memory_order_acquire)) continue; - target->drawn->store(true, std::memory_order_release); + target->hostOnly->store(true, std::memory_order_release); } if (target->fromTexture && use.cmaskAddress != 0) { target->fromTexture = false; @@ -811,14 +819,14 @@ MultisampleTarget& acquire(const Context& context, const ColorTarget& requested) target->Keep(); targets().push_back(std::move(target)); auto& created = *targets().back(); - guard(context, use, created.active, created.metadataValid, created.drawn); + guard(context, use, created.active, created.metadataValid, created.hostOnly); if (use.cmaskFastClear && use.dccAddress == 0) created.cmask.SeedCode(CurrentUniformMetadataByte(use.cmaskAddress, static_cast(use.cmaskBytes))); created.ApplyFastClear(use); created.Keep(); return created; } -MultisampleTarget& createFilled(const Context& context, const GuestTextureResource& resource, VkFormat format, const char* access) { +MultisampleTarget& createFilled(const Context& context, const GuestTextureResource& resource, VkFormat format, const char* access, bool written) { const auto refuse = [&](const std::string& reason) { char text[192]; std::snprintf(text, sizeof(text), "AGC graphics: %s the %ux%u multisampled texture 0x%llx of %u samples, which no draw rendered to and ", access, resource.width, resource.height, static_cast(resource.baseAddress), resource.samples); @@ -885,8 +893,8 @@ MultisampleTarget& createFilled(const Context& context, const GuestTextureResour target->Keep(); targets().push_back(std::move(target)); auto& created = *targets().back(); - created.drawn->store(false, std::memory_order_release); - guard(context, use, created.active, created.metadataValid, created.drawn); + created.hostOnly->store(written, std::memory_order_release); + guard(context, use, created.active, created.metadataValid, created.hostOnly); created.colorFill.NoteFill(use.address, static_cast(bytes), value); created.fromTexture = true; created.htileAddress = resource.dccAddress; @@ -1186,7 +1194,7 @@ std::shared_ptr MultisampleTargetTexture(const Context& context, const } if (other == nullptr) { Require(Recorder::Active() != nullptr, "a multisampled texture is only sampled by recorded draws and dispatches"); - return createFilled(context, resource, format, "sampling").Sampled(components, viewType); + return createFilled(context, resource, format, "sampling", false).Sampled(components, viewType); } char text[320]; std::snprintf(text, sizeof(text), "AGC graphics: sampling multisampled color target 0x%llx (%ux%u, %u samples, vk format %d) as a %ux%u texture of %u samples and guest format %u (vk %d) is not implemented", static_cast(resource.baseAddress), other->target.extent.width, other->target.extent.height, other->target.samples, static_cast(other->target.format), resource.width, resource.height, resource.samples, resource.format, static_cast(format)); @@ -1230,12 +1238,15 @@ VkImageView MultisampleTargetStorageView(const Context& context, const GuestText if (!target->active->load(std::memory_order_acquire)) continue; if (target->context.device != context.device || target->target.surfaceAddress != resource.baseAddress) continue; const auto& rendered = target->target; - if (sameStorageForm(context, rendered.format, format) && rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0) return target->Storage(StorageFormatForGuest(context, resource.format)); + if (sameStorageForm(context, rendered.format, format) && rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0) { + if (!holdHostOnly(*target)) continue; + return target->Storage(StorageFormatForGuest(context, resource.format)); + } other = target.get(); } if (other == nullptr) { Require(Recorder::Active() != nullptr, "a multisampled color target is only written by recorded dispatches"); - return createFilled(context, resource, format, "writing").Storage(StorageFormatForGuest(context, resource.format)); + return createFilled(context, resource, format, "writing", true).Storage(StorageFormatForGuest(context, resource.format)); } char text[320]; std::snprintf(text, sizeof(text), "AGC graphics: storage image access to multisampled color target 0x%llx (%ux%u, %u samples, vk format %d) as a %ux%u texture of %u samples and guest format %u (vk %d) is not implemented", static_cast(resource.baseAddress), other->target.extent.width, other->target.extent.height, other->target.samples, static_cast(other->target.format), resource.width, resource.height, resource.samples, resource.format, static_cast(format)); @@ -1275,10 +1286,13 @@ void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, bool MultisampledColorFillAbsorbs(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) { if (bytes == 0) return false; std::lock_guard lock(targetsMutex()); - return std::any_of(targets().begin(), targets().end(), [&](const auto& target) { + bool absorbed = false; + for (const auto& target : targets()) { VkClearColorValue clear{}; - return target->active->load(std::memory_order_acquire) && target->target.address == address && MultisampledColorBytes(target->target) == bytes && MultisampledFillClearColor(target->target, pattern, clear); - }); + if (!target->active->load(std::memory_order_acquire) || target->target.address != address || MultisampledColorBytes(target->target) != bytes || !MultisampledFillClearColor(target->target, pattern, clear)) continue; + absorbed = holdHostOnly(*target) || absorbed; + } + return absorbed; } void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) { @@ -1361,19 +1375,19 @@ void RequireNoMultisampleColorTarget(std::uint64_t address, std::size_t bytes, c }; { std::shared_lock lock(guardedMutex()); - bool undrawn = false; + bool retirable = false; for (const auto& range : guarded()) { if (!overlaps(range)) continue; - if (range.drawn->load(std::memory_order_acquire)) refuse(range); - undrawn = true; + if (range.hostOnly->load(std::memory_order_acquire)) refuse(range); + retirable = true; } - if (!undrawn) return; + if (!retirable) return; } std::unique_lock lock(guardedMutex()); std::vector>> retired; for (const auto& range : guarded()) { if (!overlaps(range)) continue; - if (range.drawn->load(std::memory_order_acquire)) refuse(range); + if (range.hostOnly->load(std::memory_order_acquire)) refuse(range); retired.push_back(range.active); } for (const auto& active : retired) active->store(false, std::memory_order_release); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index d679f6446f..5870d759ac 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -1045,6 +1045,23 @@ int main(int argc, char** argv) { Block undrawn(BlockBytes); StoreSamples(*device, TargetDescriptor(undrawn, SampleShift), SampleWrites(leftHalf)); CheckWritten(std::vector>(Side * Side * Samples), LoadSamples(*device, undrawn), leftHalf, "into zeroed color memory no draw rendered to"); + const auto refusedOver = [&](const Block& over) { + try { + AgcDriver::Graphics::RequireNoMultisampleColorTarget(over.Address(), static_cast(colorBytes / 2u), "a storage image access"); + } catch (const std::exception& error) { + return std::string(error.what()).find("whose samples are only on the host") != std::string::npos; + } + return false; + }; + Require(refusedOver(undrawn), "a guest access over a 4x texture no draw rendered to but a dispatch wrote is not refused"); + Block absorbing(BlockBytes); + StoreWords(absorbing, colorBytes, FillOnes); + CheckUniform(LoadSamples(*device, absorbing), Ones, "from color memory of half-float ones no draw rendered to, before a fill the texture absorbs"); + Require(AgcDriver::Graphics::MultisampledColorFillAbsorbs(absorbing.Address(), static_cast(colorBytes), 0u), "a fill of 0 over the whole color memory of a 4x texture no draw rendered to is not absorbed"); + AgcDriver::Graphics::NoteColorMetadataFill(absorbing.Address(), static_cast(colorBytes), 0u); + Require(refusedOver(absorbing), "a guest access over a 4x texture no draw rendered to that absorbed a fill is not refused"); + CheckUniform(LoadSamples(*device, absorbing), {}, "from a 4x texture no draw rendered to after it absorbed a fill of 0"); + std::puts("a guest access over a 4x texture no draw rendered to is still refused once a dispatch wrote it or it absorbed a fill"); Block mixedStore(BlockBytes); StoreWords(mixedStore, colorBytes, FillOnes); std::memcpy(mixedStore.data + colorBytes - sizeof(odd), &odd, sizeof(odd)); From a8706b22ba0f0121de4b4e477b27a7b42c8ce8e3 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:35:36 +0200 Subject: [PATCH 261/428] docs(agc): record when an undrawn multisampled target is retired on guest access --- docs/dev/debt/libSceAgcDriver.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 36535fa9b4..6513f7663b 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -84,8 +84,8 @@ - [Multisampled DCC comp-to-single clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (AGC driver) - RADV's multisampled comp-to-single clear dispatch ([`radv_clear_dcc_comp_to_single`](https://github.com/mihawk-99/PS5_Mesa/blob/1ba9874f478657ccf6da72ce55f0260164cc0bd0/src/amd/vulkan/meta/radv_meta_clear.c#L954)) is recognized by its code and not run. Its clear color (user data 5 to 8, which the shader stores as the four dwords of sample 0's texel) is recorded for the surface its image descriptor names, and DCC keys of 0x10 on a multisampled target of that surface, pipe-bank XOR, size and sample count import as a clear to it, for texels of 8 to 128 bits. A record is dropped when an unmap or allocation removal covers its surface's base. The guest's color memory doesn't get the color at the blocks' first pixels, and the recorded color is assumed to be the one the keys refer to: RADV writes 0x10 keys only in a fast clear that runs this dispatch for the same image right after, and initializes DCC to 0x00 or 0xff on an image's first use ([`radv_init_color_image_metadata`](https://github.com/mihawk-99/PS5_Mesa/blob/1ba9874f478657ccf6da72ce55f0260164cc0bd0/src/amd/vulkan/radv_cmd_buffer.c#L16894)). A new image on memory reused within one mapping, with the same address, XOR, size and sample count, whose clear runs a comp-to-single shader other than the recognized one, would still import the old color. - [Initial depth and stencil planes](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) - the console's reset values of `DB_Z_INFO` and `DB_STENCIL_INFO` (context `0x010`-`0x011`) are not measured. Both start, and return after `CLEAR_STATE`, at zero (formats `INVALID`), the value of Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h). The public [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) write `0x80000180` and `0x20000180` instead, also with formats `INVALID`; of their other bits, only `TILE_STENCIL_DISABLE` would change a result: the HTILE layout of a multisampled depth plane whose `DB_STENCIL_INFO` was never written. - [Type-0 and type-1 texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (`TYPE` = 0 or 1 in dword 3) with a nonzero base are bound as null images, like a zero base. TYPE 0 comes from a gfx1036 (RDNA2) measurement in issue #4139 (image sample, load and resinfo return zero, no fault). TYPE 1 comes from a gfx1010 (RDNA1) measurement in #4139 and #5828 (`image_sample_lz`, `image_sample_b`, `image_load` and `image_get_resinfo` return zero, no fault) with format 22 only; TYPE 1 is not measured on RDNA2, with other formats or with depth-compare reads (`image_sample_c*`). Neither type was measured on the console's gfx1013. -- [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. The next target, sampled or storage lookup removes retired cache owners; recorded work and sampled views keep their images alive until their last reference is released. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported. -- [Stencil reference export replay](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) on devices without `VK_EXT_shader_stencil_export` replays the low eight bits of the values proved from constant, select and phi exports, with an export proved on every kept path. An export the prover cannot bound replays all 256 eight-bit values, one pass per value per triangle (logged once per process). It records instances, then their triangles, then the reference classes of each triangle. One pipeline filters fragments at shader termination against a dynamic push value before late stencil and depth tests. The [Vulkan stencil-reference replacement rule](https://docs.vulkan.org/refpages/latest/refpages/source/FragStencilRefEXT.html) discards bits above the attachment's stencil width; supported stencil attachments have eight bits. Numeric subgroup observations throw except bounded own-lane mask plumbing and group control with kept-lane and derivative-helper coverage proved. WQM inputs must exclude helper contributions (a ballot of the active lanes, as in an EXEC copy, covers whole quads and is accepted), since [non-quad subgroup operations may treat helpers as inactive](https://docs.vulkan.org/glslext/latest/glslext/khr/GL_KHR_shader_subgroup.html). Readonly guarded SRT reads may report internal host faults, and read-only BDA reads may report faults; these draws wait and check faults before publishing results, and loops throw. Indirect draws, restart, strips, fans, tessellation, mesh, multisampling, early tests, ordered pixel execution, guest shader memory writes, other fault-producing memory access, clocks, primitive ID inputs, attachment feedback and pipeline face/channel splits throw. An exhausted stage push range throws. Equivalence to console ordering has not been measured on console hardware. +- [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. The next target, sampled or storage lookup removes retired cache owners; recorded work and sampled views keep their images alive until their last reference is released. A target made from a multisampled texture that a sample reads before any draw is retired instead when a guest access overlaps it, until a draw, a dispatch's storage write or an absorbed color fill puts samples on the host only; it is rebuilt from guest memory at its next use. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported. +- [Stencil reference export replay](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) on devices without `VK_EXT_shader_stencil_export` replays the low eight bits of the values proved from constant, select and phi exports, with an export proved on every kept path. An export the prover cannot bound replays all 256 eight-bit values, one pass per value per triangle (logged once per process). It records instances, then their triangles, then the reference classes of each triangle. One pipeline filters fragments at shader termination against a dynamic push value before late stencil and depth tests. The [Vulkan stencil-reference replacement rule](https://docs.vulkan.org/refpages/latest/refpages/source/FragStencilRefEXT.html) discards bits above the attachment's stencil width; supported stencil attachments have eight bits. Numeric subgroup observations throw except bounded own-lane mask plumbing and group control with kept-lane and derivative-helper coverage proved. WQM inputs must exclude helper contributions, since [non-quad subgroup operations may treat helpers as inactive](https://docs.vulkan.org/glslext/latest/glslext/khr/GL_KHR_shader_subgroup.html). Readonly guarded SRT reads may report internal host faults, and read-only BDA reads may report faults; these draws wait and check faults before publishing results, and loops throw. Indirect draws, restart, strips, fans, tessellation, mesh, multisampling, early tests, ordered pixel execution, guest shader memory writes, other fault-producing memory access, clocks, primitive ID inputs, attachment feedback and pipeline face/channel splits throw. An exhausted stage push range throws. Equivalence to console ordering has not been measured on console hardware. - [32-bit depth-bits reads of guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): a sampled 32_UINT or 32_SINT T# with a depth swizzle mode that no live depth surface matches reads the guest bytes through an R32_SFLOAT image, as the recompiler's depth-bits read expects, and the shader casts the fetched value back to the stored dword. On RX 6700 XT (gfx1031, AMD Windows driver 32.0.21045.5002) the read keeps float denormals, the sign bit alone and quiet and signaling NaN payloads bit for bit. A host that canonicalizes NaNs or flushes denormals on a 32-bit float fetch would change those dwords, and other drivers are not measured. A live depth surface whose extent or base level differs from the T# is not used; the read takes the guest bytes, as the 32-bit float and 16-bit views already do. - [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A dispatch of the 15-word 8x8x1 image clear kernel ([`MatchStorageImageClear`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp): one colour read from the V# in `user[8..11]` stored to every texel of the T# in `user[0..7]`) that covers the whole plane and stores 0 or 1 through a 16-bit unorm view, 0 or 0xffff through a 16-bit uint view, or 0 or a normal float in (0, 1] through a 32-bit float or integer view leaves out the copy-in and replaces the copy-out by a clear of the depth image to that value; the dispatch still writes the storage image. The colour is read again when the copy-out is replaced, and a changed value keeps the copy-out. Indirect dispatches, partial last groups, dispatches that queue on the draw pipeline, other values and formats, a plane the kernel covers only partly and stencil planes keep both copies. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane, or of a later slice, is a storage image over the slices from 0 to its last (`LayeredDepthStorage`); only the slices from its base slice to its last are copied: a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, base slice, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, a view from another base slice, or a sampled copy) is a separate image, copied in before and out after each use, so views that are written before a flush and share a slice do not merge: the later copy-out wins, so their results are not merged. A matching view still throws when it is a cube or a 2D array of more than one layer of the stencil plane or from a later slice, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. From 0aa8b09eb0e61a02aa1c2b28869519da78588365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 23 Oct 2026 22:21:44 +0200 Subject: [PATCH 262/428] fix(agc): pass the host-only flag at the first-sample sampling path --- .../libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index ff57768169..84ea19b4ff 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -1217,7 +1217,7 @@ std::shared_ptr MultisampleTargetFirstSample(const Context& context, co } if (other == nullptr) { Require(Recorder::Active() != nullptr, "a multisampled texture is only sampled by recorded draws and dispatches"); - return createFilled(context, resource, format, "sampling").FirstSample(components); + return createFilled(context, resource, format, "sampling", false).FirstSample(components); } char text[320]; std::snprintf(text, sizeof(text), "AGC graphics: reading sample 0 of multisampled color target 0x%llx (%ux%u, %u samples, vk format %d) as a %ux%u texture of %u samples and guest format %u (vk %d) is not implemented", static_cast(resource.baseAddress), other->target.extent.width, other->target.extent.height, other->target.samples, static_cast(other->target.format), resource.width, resource.height, resource.samples, resource.format, static_cast(format)); From b6bfc78286ad4b7ecdb2532906afceb98add42be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 24 Oct 2026 01:57:36 +0200 Subject: [PATCH 263/428] docs(agc): keep main's stencil replay debt line --- docs/dev/debt/libSceAgcDriver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 6513f7663b..4cc96f18cf 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -85,7 +85,7 @@ - [Initial depth and stencil planes](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) - the console's reset values of `DB_Z_INFO` and `DB_STENCIL_INFO` (context `0x010`-`0x011`) are not measured. Both start, and return after `CLEAR_STATE`, at zero (formats `INVALID`), the value of Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h). The public [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) write `0x80000180` and `0x20000180` instead, also with formats `INVALID`; of their other bits, only `TILE_STENCIL_DISABLE` would change a result: the HTILE layout of a multisampled depth plane whose `DB_STENCIL_INFO` was never written. - [Type-0 and type-1 texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (`TYPE` = 0 or 1 in dword 3) with a nonzero base are bound as null images, like a zero base. TYPE 0 comes from a gfx1036 (RDNA2) measurement in issue #4139 (image sample, load and resinfo return zero, no fault). TYPE 1 comes from a gfx1010 (RDNA1) measurement in #4139 and #5828 (`image_sample_lz`, `image_sample_b`, `image_load` and `image_get_resinfo` return zero, no fault) with format 22 only; TYPE 1 is not measured on RDNA2, with other formats or with depth-compare reads (`image_sample_c*`). Neither type was measured on the console's gfx1013. - [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. The next target, sampled or storage lookup removes retired cache owners; recorded work and sampled views keep their images alive until their last reference is released. A target made from a multisampled texture that a sample reads before any draw is retired instead when a guest access overlaps it, until a draw, a dispatch's storage write or an absorbed color fill puts samples on the host only; it is rebuilt from guest memory at its next use. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported. -- [Stencil reference export replay](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) on devices without `VK_EXT_shader_stencil_export` replays the low eight bits of the values proved from constant, select and phi exports, with an export proved on every kept path. An export the prover cannot bound replays all 256 eight-bit values, one pass per value per triangle (logged once per process). It records instances, then their triangles, then the reference classes of each triangle. One pipeline filters fragments at shader termination against a dynamic push value before late stencil and depth tests. The [Vulkan stencil-reference replacement rule](https://docs.vulkan.org/refpages/latest/refpages/source/FragStencilRefEXT.html) discards bits above the attachment's stencil width; supported stencil attachments have eight bits. Numeric subgroup observations throw except bounded own-lane mask plumbing and group control with kept-lane and derivative-helper coverage proved. WQM inputs must exclude helper contributions, since [non-quad subgroup operations may treat helpers as inactive](https://docs.vulkan.org/glslext/latest/glslext/khr/GL_KHR_shader_subgroup.html). Readonly guarded SRT reads may report internal host faults, and read-only BDA reads may report faults; these draws wait and check faults before publishing results, and loops throw. Indirect draws, restart, strips, fans, tessellation, mesh, multisampling, early tests, ordered pixel execution, guest shader memory writes, other fault-producing memory access, clocks, primitive ID inputs, attachment feedback and pipeline face/channel splits throw. An exhausted stage push range throws. Equivalence to console ordering has not been measured on console hardware. +- [Stencil reference export replay](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) on devices without `VK_EXT_shader_stencil_export` replays the low eight bits of the values proved from constant, select and phi exports, with an export proved on every kept path. An export the prover cannot bound replays all 256 eight-bit values, one pass per value per triangle (logged once per process). It records instances, then their triangles, then the reference classes of each triangle. One pipeline filters fragments at shader termination against a dynamic push value before late stencil and depth tests. The [Vulkan stencil-reference replacement rule](https://docs.vulkan.org/refpages/latest/refpages/source/FragStencilRefEXT.html) discards bits above the attachment's stencil width; supported stencil attachments have eight bits. Numeric subgroup observations throw except bounded own-lane mask plumbing and group control with kept-lane and derivative-helper coverage proved. WQM inputs must exclude helper contributions (a ballot of the active lanes, as in an EXEC copy, covers whole quads and is accepted), since [non-quad subgroup operations may treat helpers as inactive](https://docs.vulkan.org/glslext/latest/glslext/khr/GL_KHR_shader_subgroup.html). Readonly guarded SRT reads may report internal host faults, and read-only BDA reads may report faults; these draws wait and check faults before publishing results, and loops throw. Indirect draws, restart, strips, fans, tessellation, mesh, multisampling, early tests, ordered pixel execution, guest shader memory writes, other fault-producing memory access, clocks, primitive ID inputs, attachment feedback and pipeline face/channel splits throw. An exhausted stage push range throws. Equivalence to console ordering has not been measured on console hardware. - [32-bit depth-bits reads of guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): a sampled 32_UINT or 32_SINT T# with a depth swizzle mode that no live depth surface matches reads the guest bytes through an R32_SFLOAT image, as the recompiler's depth-bits read expects, and the shader casts the fetched value back to the stored dword. On RX 6700 XT (gfx1031, AMD Windows driver 32.0.21045.5002) the read keeps float denormals, the sign bit alone and quiet and signaling NaN payloads bit for bit. A host that canonicalizes NaNs or flushes denormals on a 32-bit float fetch would change those dwords, and other drivers are not measured. A live depth surface whose extent or base level differs from the T# is not used; the read takes the guest bytes, as the 32-bit float and 16-bit views already do. - [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A dispatch of the 15-word 8x8x1 image clear kernel ([`MatchStorageImageClear`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp): one colour read from the V# in `user[8..11]` stored to every texel of the T# in `user[0..7]`) that covers the whole plane and stores 0 or 1 through a 16-bit unorm view, 0 or 0xffff through a 16-bit uint view, or 0 or a normal float in (0, 1] through a 32-bit float or integer view leaves out the copy-in and replaces the copy-out by a clear of the depth image to that value; the dispatch still writes the storage image. The colour is read again when the copy-out is replaced, and a changed value keeps the copy-out. Indirect dispatches, partial last groups, dispatches that queue on the draw pipeline, other values and formats, a plane the kernel covers only partly and stencil planes keep both copies. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane, or of a later slice, is a storage image over the slices from 0 to its last (`LayeredDepthStorage`); only the slices from its base slice to its last are copied: a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, base slice, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, a view from another base slice, or a sampled copy) is a separate image, copied in before and out after each use, so views that are written before a flush and share a slice do not merge: the later copy-out wins, so their results are not merged. A matching view still throws when it is a cube or a 2D array of more than one layer of the stencil plane or from a later slice, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. From e537509b7db908bb0fef8c993960f5376aba0c34 Mon Sep 17 00:00:00 2001 From: Bram910 Date: Sat, 24 Oct 2026 22:42:38 +0200 Subject: [PATCH 264/428] test(tools): resolve JobManager artifact path expectations --- tools/tests/test_unwrap_self.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index 1d695d2a4b..b8bcdd092f 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -1077,11 +1077,11 @@ class JobManagerModuleRefusalTests(unittest.TestCase): for index, (data, diagnostic) in enumerate(cases): with self.subTest(diagnostic=diagnostic): self.module.write_bytes(data) - output = self.root / f"artifacts-{index}" + output = self.build / ".." / f"artifacts-{index}" code, stdout, stderr, process = self.invoke(output) self.assertEqual(code, 2) self.assertEqual(stderr, diagnostic + "\n") - self.assertIn(f"Artifacts: {output}", stdout) + self.assertEqual(stdout, f"Artifacts: {output.resolve()}\n") self.assertNotIn("Traceback", stderr) self.assertEqual(self.module.read_bytes(), data) self.assertEqual((output / "libs" / "jobref.prx").read_bytes(), b"reference helper") @@ -1089,7 +1089,7 @@ class JobManagerModuleRefusalTests(unittest.TestCase): self.assertFalse((output / "relink.log").exists()) self.assertFalse((output / "results.json").exists()) process.assert_called_once() - self.assertEqual(process.call_args.args[0][0], str(self.build / "core/libs/nid_patcher.exe")) + self.assertEqual(process.call_args.args[0][0], str(self.build.resolve() / "core/libs/nid_patcher.exe")) def test_missing_reference_helper_keeps_existing_error_handling(self): self.helper.unlink() From 76e32bca2da02719849f359bc89a1afba3e1a5cf Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Sat, 24 Oct 2026 22:51:06 +0200 Subject: [PATCH 265/428] fix(relinker): reject conflicting duplicates of the dynamic tags the pipeline reads getTagValue returned the first entry of a dynamic tag, so an executable with DT_RELASZ 0 followed by DT_RELASZ 24 converted for Linux with exit 0 and an output without DT_RELA, while the reverse order kept the relocation. getTagValue now scans every entry and throws "Conflicting duplicate tags" when a tag it is asked for repeats with a different value, the rule RelativeRelocationIndex already applies with unused-filter=1. The pipeline check accepts a repeat with the same value: it is not ambiguous, and #1938, which rejected it, was closed because no title was shown and because rejecting it could refuse an input the console loads. The code analysis on Windows and macOS and the strict filter keep their own duplicate checks. Inputs without such a conflict convert as before, and an input with one now fails with the same message on every target. --- .../src/pipeline/RelinkerPipeline.cpp | 13 +++- .../relinker/relinker/tests/test_rela_size.py | 70 ++++++++++++++++--- 2 files changed, 71 insertions(+), 12 deletions(-) diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 77044446d2..f992b9ebf7 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -120,9 +120,6 @@ RelinkResult RelinkerPipeline::Relink() { }; auto getTagValue = [&](const std::int64_t tag) -> std::uint64_t { - for (const auto& t : dynTags) - if (t.Tag == tag) - return t.Value; const char* name = "unknown"; switch (tag) { case DT_OS_STRTAB: name = "DT_OS_STRTAB"; break; @@ -149,6 +146,16 @@ RelinkResult RelinkerPipeline::Relink() { case DT_RELAENT: name = "DT_RELAENT"; break; case DT_OS_SYMTABSZ: name = "DT_OS_SYMTABSZ"; break; } + std::optional value; + for (const auto& t : dynTags) { + if (t.Tag != tag) + continue; + if (value && *value != t.Value) + throw RelinkerException(std::string("Conflicting duplicate ") + name + " tags"); + value = t.Value; + } + if (value) + return *value; std::ostringstream message; message << "Required dynamic tag " << name << " (0x" << std::hex << tag << ") not found"; throw RelinkerException(message.str()); diff --git a/core/relinker/relinker/tests/test_rela_size.py b/core/relinker/relinker/tests/test_rela_size.py index d010e23814..9d7bd5d6e8 100644 --- a/core/relinker/relinker/tests/test_rela_size.py +++ b/core/relinker/relinker/tests/test_rela_size.py @@ -7,6 +7,24 @@ import tempfile from test_dynamic_segment import fixture +def output_tags(output): + data = output.read_bytes() + header_offset = struct.unpack_from(" Date: Wed, 21 Oct 2026 17:55:39 -0400 Subject: [PATCH 266/428] perf(agc): mirror a sparse heap whose committed span crosses adjacent ranges A sparse heap the guest grows by mapping an adjacent range is no longer covered by one leased range, so bindSparseMirror refused it and every use copied the heap's committed pieces on the CPU. The mirror now takes a joined range over the run of continuing ranges (crossedRun), kept while its member ranges stay the same. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/GuestBufferMemory.cpp | 64 ++++++++++++++++++- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 12939ea058..8c5de5f4d6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -2202,6 +2202,39 @@ void unlistMirror(ImageMirrors& state, ImageMirror& mirror) { mirror.listed = false; } +struct JoinedHeapRange { + std::vector> members; + std::shared_ptr range; +}; + +struct JoinedHeapRanges { + std::mutex mutex; + std::map entries; +}; + +JoinedHeapRanges& JoinedHeaps() { + static JoinedHeapRanges ranges; + return ranges; +} + +bool releaseExpiredJoinedHeaps() { + std::vector> released; + { + auto& joined = JoinedHeaps(); + std::lock_guard lock(joined.mutex); + for (auto it = joined.entries.begin(); it != joined.entries.end();) { + const auto& members = it->second.members; + if (std::none_of(members.begin(), members.end(), [](const auto& member) { return member.expired(); })) { + ++it; + continue; + } + released.push_back(std::move(it->second.range)); + it = joined.entries.erase(it); + } + } + return !released.empty(); +} + std::uint64_t blockBytes(const GuestRuns& runs) { constexpr std::uint64_t block = ResidentBuffer::BlockBytes; std::uint64_t blocks = 0; @@ -2887,9 +2920,11 @@ std::shared_ptr findMirror(const Context& context, std::uint64_t be // Drops mirrors whose registered range is gone (a later build made a new one for the replacement). void sweepMirrors() { + const bool released = releaseExpiredJoinedHeaps(); const auto space = Spaces().current.load(); auto& state = Mirrors(); std::lock_guard lock(state.mutex); + if (released) ++state.version; if (space != nullptr && state.pinnedSpace == space->serial && state.pinnedVersion == state.version) return; ++state.sweeps; bool pinned = space != nullptr; @@ -4270,15 +4305,38 @@ bool GuestBufferMemory::sparseHeapCandidate(const Region& region) { return region.sparseHeap && region.sparse && region.mirror == nullptr && region.direct == nullptr && (region.hostBacked || region.writable) && mirrorsEnabled() && sparseMirrorsEnabled(); } +std::shared_ptr joinedHeapRange(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { + const auto run = crossedRun(lease, begin, end); + if (run.ranges.size() < 2 || std::any_of(run.ranges.begin(), run.ranges.end(), [](const auto& range) { return !range->releasable; })) return nullptr; + releaseExpiredJoinedHeaps(); + auto& joined = JoinedHeaps(); + std::lock_guard lock(joined.mutex); + auto& entry = joined.entries[run.base]; + bool same = entry.range != nullptr && entry.members.size() == run.ranges.size(); + for (std::size_t index = 0; same && index < run.ranges.size(); ++index) same = sameRange(entry.members[index], run.ranges[index]); + if (!same) { + const auto& first = *run.ranges.front(); + entry.members.assign(run.ranges.begin(), run.ranges.end()); + entry.range = std::make_shared(GuestAllocations::Range{run.base, static_cast(run.bytes), true, first.writable, first.allocationAddress, first.allocationBytes, true, first.gpu, first.backingSerial}); + } + return entry.range; +} + bool GuestBufferMemory::bindSparseMirror(Region& region, const GuestAllocations::Lease& lease) { const auto backedEnd = region.backed.empty() ? region.begin : region.backed.back().second; const auto neededEnd = std::min(region.end, std::max(backedEnd, std::max(region.lastView, region.begin) + 4u)); - const auto* range = containingRange(lease, region.begin, neededEnd); - if (range == nullptr || !range->releasable) return false; + std::shared_ptr owner; + if (const auto* range = containingRange(lease, region.begin, neededEnd); range != nullptr) { + if (!range->releasable) return false; + owner = leasedRangeOwner(lease, range->address); + } else { + owner = joinedHeapRange(lease, region.begin, neededEnd); + if (owner == nullptr) return false; + } std::vector blocks; std::shared_ptr mirror; try { - mirror = acquireMirror(context, leasedRangeOwner(lease, range->address), blocks, true); + mirror = acquireMirror(context, owner, blocks, true); } catch (const std::runtime_error& error) { static std::atomic reported{0}; if (reported.fetch_add(1, std::memory_order_relaxed) < 4) std::fprintf(stderr, "[gpu] %s; copying the sparse heap instead\n", error.what()); From 46a155d7edcd7b88c7f2e92f395e8ba6e8bbf09b Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 21 Oct 2026 18:05:02 -0400 Subject: [PATCH 267/428] test(agc): mirror a sparse heap whose committed span crosses adjacent ranges Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/BdaResources.cpp | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index c2c44c6aed..958b2e50dd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -3443,6 +3443,87 @@ void hostWriteWatchExclusionTests() { #endif } +void sparseJoinedHeapMirrorTests(const Context& context, const BdaTestAccess& access) { + constexpr std::size_t half = 1u << 20u; + constexpr std::size_t reserved = 2 * half; + constexpr std::size_t page = 65536; +#ifdef _WIN32 + if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) { + std::cout << "guest arena unavailable or not write-watched: sparse heap mirrors over adjacent ranges not tested\n"; + return; + } + void* block = GuestArena::GuestArenaAllocate_nid_postfix(reserved, page); + auto* guest = static_cast(block); + GuestArena::GuestArenaCommit_nid_postfix(guest, page, PAGE_READWRITE, page); + GuestArena::GuestArenaCommit_nid_postfix(guest + half, page, PAGE_READWRITE, page); +#else + if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) { + std::cout << "write watch unavailable: sparse heap mirrors over adjacent ranges not tested\n"; + return; + } + void* mapping = mmap(nullptr, reserved + page, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(mapping != MAP_FAILED, "cannot reserve the sparse heap over adjacent ranges"); + auto* guest = reinterpret_cast((reinterpret_cast(mapping) + page - 1) & ~std::uintptr_t{page - 1}); + Require(mprotect(guest, page, PROT_READ | PROT_WRITE) == 0 && mprotect(guest + half, page, PROT_READ | PROT_WRITE) == 0, "cannot commit the sparse heap pages"); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(guest, reserved); + void* block = guest; +#endif + auto* second = guest + half; + std::memset(guest, 0x5a, page); + std::memset(second, 0x6b, page); + const auto base = reinterpret_cast(block); + const auto registry = [&](bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) { + GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, guest, half, true, true, true); + GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, second, half, true, true, true); + } else { + GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, guest); + GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, second); + } + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + const auto sweep = [&] { + GuestBufferMemory memory(context); + memory.AcquireRegistered(); + memory.Upload(true); + memory.WriteBack(); + }; + sweep(); + registry(true); + const auto bind = [&] { + GuestBufferMemory memory(context); + memory.AddWritable(base, reserved); + memory.Upload(false); + std::uint32_t adjustment = 0; + const auto view = memory.Descriptor(base, reserved, adjustment); + Require(adjustment == 0, "a sparse heap view over adjacent ranges is adjusted"); + const auto bytes = access.bytes(view.buffer); + Require(view.range >= half + page && bytes[view.offset + page - 1] == std::byte{0x5a} && bytes[view.offset + page] == std::byte{} && bytes[view.offset + half] == std::byte{0x6b}, "a sparse heap over adjacent ranges is not bound to its committed extent"); + bytes[view.offset + half + 8] = std::byte{0x77}; + memory.WriteBack(); + return view; + }; + const auto before = MirrorCounters(); + const auto first = bind(); + const auto made = MirrorCounters(); + Require(made.sparseBinds == before.sparseBinds + 1 && made.heapMirrors == before.heapMirrors + 1, "a sparse heap whose committed span crosses an adjacent range was not mirrored"); + Require(second[8] == 0x77, "a store to the adjacent range of a mirrored sparse heap was not written back"); + second[8] = 0x6b; + const auto again = bind(); + Require(again.buffer == first.buffer && MirrorCounters().rebuilds == made.rebuilds && MirrorCounters().sparseBinds == made.sparseBinds + 1, "a sparse heap over unchanged adjacent ranges did not reuse its mirror"); + registry(false); + sweep(); + Require(MirrorCounters().heapMirrors == before.heapMirrors && MirrorCounters().heapBytes == before.heapBytes, "the sparse heap mirror over adjacent ranges outlived its ranges"); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, reserved); + GuestArena::GuestArenaRelease_nid_postfix(block, reserved); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(guest, reserved); + munmap(mapping, reserved + page); +#endif +} + void sparseHeapMirrorTests(const Context& context, const BdaTestAccess& access) { #ifdef _WIN32 if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) { @@ -3851,6 +3932,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { spaceCommittedPagesTests(context, access); hostWriteWatchExclusionTests(); sparseHeapMirrorTests(context, access); + sparseJoinedHeapMirrorTests(context, access); Require(AddressCopyOverflow({{0x1000, 0x3000, 0x2000, "uncommitted pages"}}, 0x2000).empty(), "copies within the limit were refused"); const auto copies = AddressCopyOverflow({{0x1000, 0x2000, 0x1000, "not mirrored"}, {0x10000, 0x30000, 0x18000, "uncommitted pages"}}, 0x2000); Require(!copies.empty() && copies.find("0x10000+0x20000 (0.1 MiB committed, uncommitted pages)") < copies.find("0x1000+0x1000"), "the copy limit does not name the largest copy first"); From 7fd7ae406444f2b0fbe18a5531a255e8d0dfec31 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 22 Oct 2026 06:35:03 -0400 Subject: [PATCH 268/428] perf(agc): keep a heap mirror that an import covers only in part An address-based build releases the heap mirror at the base of each range it imports. A mirror over a joined run of ranges starts at its first range but spans the others too, so importing that range alone released it, and the next descriptor bind rebuilt it: in Echoes of the End, 140 releases and rebuilds of a 544 MiB mirror in 480 s. A heap mirror is now released only when the imported range covers its reserved span. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/GuestBufferMemory.cpp | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 8c5de5f4d6..2d822b3976 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -69,6 +69,7 @@ struct ImageMirror { std::uint64_t bytes = 0; bool writable = false; bool heap = false; + bool joinedHeap = false; bool sparse = false; std::uint64_t reserved = 0; std::vector> committed; @@ -2744,7 +2745,7 @@ bool importsNow(const Context& context, HostImports& state, const GuestAllocatio // The mirror for a leased image range: the existing one, or a new one when none exists or the // registered Range object changed (a protection change). Null when the range cannot be mirrored. An // existing writable mirror's blocks are appended to `blocks` for the caller's one compare of every -std::shared_ptr acquireMirror(const Context& context, const std::shared_ptr& range, std::vector& blocks, bool heap, std::uint64_t begin = 0, std::uint64_t bytes = 0) { +std::shared_ptr acquireMirror(const Context& context, const std::shared_ptr& range, std::vector& blocks, bool heap, std::uint64_t begin = 0, std::uint64_t bytes = 0, bool joinedHeap = false) { auto selected = *range; if (bytes != 0) { Require(begin >= range->address && bytes <= range->bytes && begin - range->address <= range->bytes - bytes, "heap mirror selection exceeds its registered range"); @@ -2765,7 +2766,7 @@ std::shared_ptr acquireMirror(const Context& context, const std::sh } if (state.failed.contains(selected.address)) return nullptr; const auto found = state.entries.find(selected.address); - if (found != state.entries.end() && sameRange(found->second->range, range) && found->second->reserved == selected.bytes && found->second->writable == range->writable && found->second->heap == heap) mirror = found->second; + if (found != state.entries.end() && sameRange(found->second->range, range) && found->second->reserved == selected.bytes && found->second->writable == range->writable && found->second->heap == heap && found->second->joinedHeap == joinedHeap) mirror = found->second; } if (mirror != nullptr) { if (!mirror->sparse) { @@ -2788,6 +2789,7 @@ std::shared_ptr acquireMirror(const Context& context, const std::sh mirror->sparse = sparse; mirror->writable = range->writable; mirror->heap = heap; + mirror->joinedHeap = joinedHeap; mirror->range = range; GuestRuns backed; if (heap) { @@ -2948,6 +2950,14 @@ void sweepMirrors() { void releaseImportedMirrors(const Context& context, std::span> imported) { if (imported.empty()) return; + std::vector> coverage(imported.begin(), imported.end()); + std::sort(coverage.begin(), coverage.end()); + std::size_t count = 0; + for (const auto& interval : coverage) { + if (count != 0 && interval.first <= coverage[count - 1].second) coverage[count - 1].second = std::max(coverage[count - 1].second, interval.second); + else coverage[count++] = interval; + } + coverage.resize(count); std::vector> released; auto& state = Mirrors(); std::lock_guard lock(state.mutex); @@ -2961,6 +2971,13 @@ void releaseImportedMirrors(const Context& context, std::spanjoinedHeap) { + const auto covered = std::upper_bound(coverage.begin(), coverage.end(), mirror->base, [](std::uint64_t address, const auto& interval) { return address < interval.first; }); + if (covered == coverage.begin() || std::prev(covered)->second < mirror->base + mirror->reserved) { + ++it; + continue; + } + } ++state.heapImported; state.heapImportedBytes += mirror->accounted; unlistMirror(state, *mirror); @@ -4326,17 +4343,19 @@ bool GuestBufferMemory::bindSparseMirror(Region& region, const GuestAllocations: const auto backedEnd = region.backed.empty() ? region.begin : region.backed.back().second; const auto neededEnd = std::min(region.end, std::max(backedEnd, std::max(region.lastView, region.begin) + 4u)); std::shared_ptr owner; + bool joined = false; if (const auto* range = containingRange(lease, region.begin, neededEnd); range != nullptr) { if (!range->releasable) return false; owner = leasedRangeOwner(lease, range->address); } else { + joined = true; owner = joinedHeapRange(lease, region.begin, neededEnd); if (owner == nullptr) return false; } std::vector blocks; std::shared_ptr mirror; try { - mirror = acquireMirror(context, owner, blocks, true); + mirror = acquireMirror(context, owner, blocks, true, 0, 0, joined); } catch (const std::runtime_error& error) { static std::atomic reported{0}; if (reported.fetch_add(1, std::memory_order_relaxed) < 4) std::fprintf(stderr, "[gpu] %s; copying the sparse heap instead\n", error.what()); From bee2bee10be9fb942b8365acc6f45fc9802015bd Mon Sep 17 00:00:00 2001 From: SlugZero <2014249+slugzero@users.noreply.github.com> Date: Sat, 24 Oct 2026 23:53:12 +0200 Subject: [PATCH 269/428] fix(libScePad): accept special handles in scePadGetControllerType Accept valid special handles in scePadGetControllerType by using KnownHandle, reporting PAD_CONTROLLER_TYPE_UNKNOWN (0) for special handles with no physical controller behind them. --- core/libs/prx/libScePad/Export.cpp | 4 ++-- core/libs/prx/libScePad/include/Pad.hpp | 1 + core/libs/tests/GuestPad.cpp | 6 ++++++ docs/dev/debt/libScePad.md | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 15cc1a4805..6c7291770b 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -706,8 +706,8 @@ int APS5_VABI scePadGetVersionInfo(std::int32_t handle, PadVersionInfo* info) { int APS5_VABI scePadGetControllerType(std::int32_t handle, std::int32_t* type) { if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; if (type == nullptr) return PAD_ERROR_INVALID_ARG; - if (PortOf(handle) < 0) return PAD_ERROR_INVALID_HANDLE; - *type = PAD_CONTROLLER_TYPE_DUAL_SENSE; + if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; + *type = SpecialHandle(handle) ? PAD_CONTROLLER_TYPE_UNKNOWN : PAD_CONTROLLER_TYPE_DUAL_SENSE; return PAD_OK; } diff --git a/core/libs/prx/libScePad/include/Pad.hpp b/core/libs/prx/libScePad/include/Pad.hpp index 03958f4eed..3ceea13e01 100644 --- a/core/libs/prx/libScePad/include/Pad.hpp +++ b/core/libs/prx/libScePad/include/Pad.hpp @@ -28,6 +28,7 @@ constexpr int PAD_DEVICE_TYPE_DUAL_SHOCK_4 = 2; constexpr int PAD_DEVICE_TYPE_DUAL_SENSE = 3; constexpr int PAD_DEVICE_TYPE_DUAL_SENSE_EDGE = 4; +constexpr int PAD_CONTROLLER_TYPE_UNKNOWN = 0; constexpr int PAD_CONTROLLER_TYPE_DUAL_SENSE = 2; constexpr int PAD_CONNECT_TYPE_UNREGISTERED = 0; diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 36e3e9e0ab..fe16df23b8 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -354,6 +354,9 @@ static void CheckSpecialPort(int user, int handle) { Require(extended.deviceClass == -1 && extended.reserved[0] == 0); Require(scePadDeviceClassGetExtendedInformation(handle, &extended) == PAD_OK); Require(extended.deviceClass == 0); + std::int32_t specialType = -1; + Require(scePadGetControllerType(special, &specialType) == PAD_OK); + Require(specialType == PAD_CONTROLLER_TYPE_UNKNOWN); const PadVibrationParam vibration{0, 0}; const PadLightBarParam lightBar{0, 0, 255}; Require(scePadSetVibration(special, nullptr) == PAD_ERROR_INVALID_ARG); @@ -716,6 +719,9 @@ int main() { std::uint32_t p2AccessState = 7; Require(scePadGetAccessibilityControllerState(handle2, &p2AccessState) == PAD_OK); Require(p2AccessState == 0); + std::int32_t p2Type = -1; + Require(scePadGetControllerType(handle2, &p2Type) == PAD_OK); + Require(p2Type == PAD_CONTROLLER_TYPE_DUAL_SENSE); PadSetPortConnected_nid_postfix(1, true); Require(scePadGetInfo_nid_postfix(&info) == PAD_OK); diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 000b382ce7..c24ae4de91 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -23,7 +23,7 @@ - [scePadIsDS4Connected, scePadIsBlasterConnected and scePadIsMoveConnected](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `bool (int handle)` signatures are recovered; the library reports one DualSense to every title (see Touchpad resolution below), so no DualShock 4, Blaster or Move controller is ever behind a handle and each call answers false for any handle. The console's answer for an invalid handle is not measured - [scePadSetAngularVelocityBiasCorrectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled - [scePadGetAccessibilityControllerState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad, which is not an Access controller, reports 0. The name hashes to NID `zBsgF0q8DIM`. In the OS 10.01 system module the `(int handle, uint32_t* state)` function returns `0x80920005` before `scePadInit`, then `INVALID_ARG` for a NULL `state` and `INVALID_HANDLE` for a bad handle, and otherwise asks the pad driver for a per-device value of the device behind the handle: 0 when no device is behind it, 1 when the driver has no answer, else the driver's value. What the values mean is unknown -- [scePadGetControllerType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad reports `PAD_CONTROLLER_TYPE_DUAL_SENSE` (2), following [duaLib](https://github.com/WujekFoliarz/duaLib/blob/master/src/include/duaLib.h)'s `s_SceControllerType` enum where `DUALSENSE = 2`. The signature `(int handle, int32_t* type)` follows duaLib, while the validation order (checking initialization, null pointer, then handle) follows the library's other pad queries; duaLib does not validate null pointers and returns `PAD_ERROR_DEVICE_NOT_CONNECTED` for an invalid pad. +- [scePadGetControllerType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad reports `PAD_CONTROLLER_TYPE_DUAL_SENSE` (2), following [duaLib](https://github.com/WujekFoliarz/duaLib/blob/master/src/include/duaLib.h)'s `s_SceControllerType` enum where `UNKNOWN = 0` and `DUALSENSE = 2`; special handles report 0 (`UNKNOWN`). The signature `(int handle, int32_t* type)` follows duaLib, while the validation order (checking initialization, null pointer, then handle) follows the library's other pad queries; duaLib does not validate null pointers and returns `PAD_ERROR_DEVICE_NOT_CONNECTED` for an invalid pad. - [libScePad internal data and device control](../../../core/libs/prx/libScePad/Export.cpp) - kernel-owned version, telemetry, Lotus-button and update payload fields remain unresolved. Lotus-button parameter units and the three blinking byte meanings are unknown. Port information occupies 152 bytes; history records occupy 72 bytes; Leonardo unique data occupies 24 bytes. Their complete field semantics remain unresolved. - [libScePad profile writes and virtual devices](../../../core/libs/prx/libScePad/Export.cpp) - profile writes take a handle, profile ID, profile pointer and a 32-bit result pointer; the profile payload direction and fields remain unresolved. Virtual-device creation takes an input record and an output ID pointer, whose width is not exposed by the wrapper. Component information copies 16 bytes; field meanings remain unknown. Virtual trigger state contains two 32-bit states and three control bytes, but the control semantics remain incomplete. From a99150abfcaaa9ef7fa26f314826c9cb680a6a30 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 22 Oct 2026 07:47:25 -0400 Subject: [PATCH 270/428] test(agc): keep a joined heap mirror when an address build imports one of its ranges Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/BdaResources.cpp | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 958b2e50dd..170a0c124f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -3524,6 +3524,119 @@ void sparseJoinedHeapMirrorTests(const Context& context, const BdaTestAccess& ac #endif } +void joinedHeapMirrorPartialImportTests(const Context& context, const BdaTestAccess& access) { + constexpr std::size_t half = 1u << 20u; + constexpr std::size_t committed = 2 * half; + constexpr std::size_t reserved = 4 * half; + constexpr std::size_t page = 65536; +#ifdef _WIN32 + if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) { + std::cout << "guest arena unavailable or not write-watched: joined heap mirrors under a partial import not tested\n"; + return; + } + void* block = GuestArena::GuestArenaAllocate_nid_postfix(reserved, half); + auto* guest = static_cast(block); + GuestArena::GuestArenaCommit_nid_postfix(guest, committed, PAGE_READWRITE, page); +#else + if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) { + std::cout << "write watch unavailable: joined heap mirrors under a partial import not tested\n"; + return; + } + void* mapping = mmap(nullptr, reserved + half, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(mapping != MAP_FAILED, "cannot reserve the joined heap"); + auto* guest = reinterpret_cast((reinterpret_cast(mapping) + half - 1) & ~std::uintptr_t{half - 1}); + Require(mprotect(guest, committed, PROT_READ | PROT_WRITE) == 0, "cannot commit the joined heap ranges"); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(guest, reserved); + void* block = guest; +#endif + std::memset(guest, 0x3c, committed); + const auto base = reinterpret_cast(block); + const auto registry = [&](bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) { + GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, guest, half, true, true, true); + GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, guest + half, half, true, true, true); + } else { + GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, guest); + GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, guest + half); + } + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + const auto sweep = [&](const Context& with) { + GuestBufferMemory memory(with); + memory.AcquireRegistered(); + memory.Upload(true); + memory.WriteBack(); + }; + const auto bind = [&] { + GuestBufferMemory memory(context); + memory.AddWritable(base, reserved); + memory.Upload(false); + std::uint32_t adjustment = 0; + const auto view = memory.Descriptor(base, reserved, adjustment); + Require(adjustment == 0 && access.bytes(view.buffer)[view.offset + half + 32] == std::byte{0x3c}, "a heap bound across two committed ranges did not read its second range"); + memory.WriteBack(); + return view; + }; + sweep(context); + registry(true); + const auto before = MirrorCounters(); + const auto first = bind(); + const auto made = MirrorCounters(); + Require(made.heapMirrors == before.heapMirrors + 1 && made.sparseBinds == before.sparseBinds + 1, "a heap bound across two committed ranges was not mirrored over both"); + auto importing = context; + importing.hostImportAlignment = page; + SetHostImportWindow(half); + SetHostImportRangeWindow(half); + SetHostImportBudget(HostImportLiveBytes() + half); + { + GuestBufferMemory memory(importing); + memory.AcquireRegistered(); + Require(ExistingHostImportFor(importing, base, half) != nullptr && ExistingHostImportFor(importing, base + half, half) == nullptr, "the joined heap partial-import fixture did not import exactly its first range"); + Require(HeapMirrorsWithin(base, base + half).mirrors == 1, "an address build that imported one range of a joined heap mirror released the mirror"); + memory.Upload(true); + memory.WriteBack(); + } + const auto partial = MirrorCounters(); + const auto again = bind(); + Require(again.buffer == first.buffer && MirrorCounters().rebuilds == partial.rebuilds, "a joined heap mirror was rebuilt after an import of one of its ranges"); + { + GuestBufferMemory memory(importing); + memory.AcquireRegistered(); + memory.AddWritable(base + half, 32); + memory.Upload(true); + if (ExistingHostImportFor(importing, base + half, 32) != nullptr) guest[half] = 0x71; + else { + std::uint32_t adjustment = 0; + const auto view = memory.Descriptor(base + half, 32, adjustment); + access.bytes(view.buffer)[view.offset + adjustment] = std::byte{0x71}; + } + guest[half + 8] = 0x52; + memory.WriteBack(); + } + Require(guest[half] == 0x71 && guest[half + 8] == 0x52, "a joined heap address build lost GPU or genuine CPU stores"); + const auto refreshed = bind(); + Require(refreshed.buffer == first.buffer && access.bytes(refreshed.buffer)[refreshed.offset + half] == std::byte{0x71} && access.bytes(refreshed.buffer)[refreshed.offset + half + 8] == std::byte{0x52}, "the retained joined mirror did not refresh stores from an address build"); + SetHostImportBudget(HostImportLiveBytes() + committed); + GuestAllocations::GuestAllocationsEnd_nid_postfix(GuestAllocations::GuestAllocationsBegin_nid_postfix()); + sweep(importing); + Require(ExistingHostImportFor(importing, base, half) != nullptr && ExistingHostImportFor(importing, base + half, half) != nullptr, "the joined heap full-import fixture did not import both ranges"); + Require(HeapMirrorsWithin(base, base + committed).mirrors == 0, "imports covering every joined heap range left a heap mirror allocated"); + SetHostImportBudget(0); + SetHostImportWindow(0); + SetHostImportRangeWindow(std::uint64_t{64} << 20u); + ClearHostImports(context.device); + registry(false); + sweep(context); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, reserved); + GuestArena::GuestArenaRelease_nid_postfix(block, reserved); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(guest, reserved); + munmap(mapping, reserved + half); +#endif +} + void sparseHeapMirrorTests(const Context& context, const BdaTestAccess& access) { #ifdef _WIN32 if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) { @@ -3933,6 +4046,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { hostWriteWatchExclusionTests(); sparseHeapMirrorTests(context, access); sparseJoinedHeapMirrorTests(context, access); + joinedHeapMirrorPartialImportTests(context, access); Require(AddressCopyOverflow({{0x1000, 0x3000, 0x2000, "uncommitted pages"}}, 0x2000).empty(), "copies within the limit were refused"); const auto copies = AddressCopyOverflow({{0x1000, 0x2000, 0x1000, "not mirrored"}, {0x10000, 0x30000, 0x18000, "uncommitted pages"}}, 0x2000); Require(!copies.empty() && copies.find("0x10000+0x20000 (0.1 MiB committed, uncommitted pages)") < copies.find("0x1000+0x1000"), "the copy limit does not name the largest copy first"); From 42961581e4c5dbc80fd860d493cc6d727ef2e318 Mon Sep 17 00:00:00 2001 From: harisibrar1 <207127467+harisibrar1@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:44:20 -0400 Subject: [PATCH 271/428] test(relinker): read the .desktop file as UTF-8 in input_preservation Co-Authored-By: Claude Opus 5.5 --- core/relinker/relinker/tests/test_input_preservation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/relinker/relinker/tests/test_input_preservation.py b/core/relinker/relinker/tests/test_input_preservation.py index 4fb96462f0..37f9ba92e9 100644 --- a/core/relinker/relinker/tests/test_input_preservation.py +++ b/core/relinker/relinker/tests/test_input_preservation.py @@ -375,7 +375,7 @@ def main(): raise AssertionError((windows, existing, result.returncode, result.stdout, result.stderr)) if not windows: assert Path(f"{output}.png").read_bytes() == image - assert Path(f"{output}.desktop").read_text().startswith("[Desktop Entry]\n") + assert Path(f"{output}.desktop").read_text(encoding="utf-8").startswith("[Desktop Entry]\n") print("Input preservation tests passed") From 9539761192c05537ebeb522ec4c8769328644753 Mon Sep 17 00:00:00 2001 From: hypermist Date: Sun, 25 Oct 2026 12:06:11 +1300 Subject: [PATCH 272/428] refactor(libSceFont): declare sceFontSetGraphicsErrorCallback in the library header Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceFont/include/Api.hpp | 336 +++++++++++++++++++++++ core/libs/tests/GuestFontGraphics.cpp | 19 +- 2 files changed, 337 insertions(+), 18 deletions(-) create mode 100644 core/libs/prx/libSceFont/include/Api.hpp diff --git a/core/libs/prx/libSceFont/include/Api.hpp b/core/libs/prx/libSceFont/include/Api.hpp new file mode 100644 index 0000000000..2e7d3d5ca6 --- /dev/null +++ b/core/libs/prx/libSceFont/include/Api.hpp @@ -0,0 +1,336 @@ +#pragma once + +#include "prx/libSceFont/include/FontTypes.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct FontGraphicsDevice; + +struct FontGraphicsDeviceParam; + +struct FontGraphicsService; + +struct FontGraphicsServiceParam; + +struct FontWords; + +struct FontGlyphSupport; + +struct FontOutlineProfile; + +struct FontMetrics; + +struct FontStyleInformation; + +struct FontGlyphExpandBufferState; + +struct FontGraphicsResource; + +struct FontGraphicsFrame; + +struct FontGraphicsDesignResource; + +struct FontGraphicsDesign; + +struct FontGraphicsCanvas; + +struct FontGraphicsCanvasSequence; + +struct FontGraphicsSurface; + +struct FontGraphicsSurfaceTexture; + +struct FontGraphicsFill; + +struct FontGraphicsRegion { + float x; + float y; + float width; + float height; + std::uint32_t flags; + std::uint32_t tag; + float outerRadius; + float innerRadius; +}; + +struct FontGraphicsFillMethod { + std::int32_t mode; + std::int32_t type; + std::uint32_t flags; + std::uint32_t tag; + std::byte reserved[16]; +}; + +struct FontGraphicsFillPlot { + float mapping[4]; + const FontGraphicsRegion* region; + std::uint16_t tag; + std::uint16_t reserved26; + std::uint16_t layout; + std::uint8_t flags; + std::byte reserved[33]; +}; + +struct FontGraphicsFillRates { + float mapping[4]; + const FontGraphicsFill* fill; + std::uint16_t tag; + std::uint16_t effect; + std::uint16_t layout; + std::uint8_t flags; + std::byte reserved[33]; +}; + +struct FontGraphicsPoolUsage { + std::uint32_t capacity; + std::uint32_t used; + std::uint32_t available; + std::uint32_t peak; + std::int32_t change; +}; + +struct FontGraphicsMemoryUsage { + FontGraphicsPoolUsage storage; + FontGraphicsPoolUsage allocations; + void* address; +}; + +struct FontGraphicsDeviceUsage { + FontGraphicsMemoryUsage first; + FontGraphicsMemoryUsage second; +}; + +namespace Font { + +using GraphicsErrorCallback = void (APS5_VABI*)(int error); + +} + +extern "C" { + +int APS5_VABI sceFontMemoryInit(FontMemory* memory, void* regionAddress, std::uint32_t regionSize, const FontMemoryInterface* iface, void* mspaceObject, FontMemoryDestroyFunction destroyCallback, void* destroyObject); +int APS5_VABI sceFontMemoryTerm(FontMemory* memory); +int APS5_VABI sceFontCreateLibraryWithEdition(const FontMemory* memory, const void* createParams, std::uint64_t edition, FontLibrary* pLibrary); +int APS5_VABI sceFontCreateLibrary(const FontMemory* memory, const void* createParams, FontLibrary* pLibrary); +int APS5_VABI sceFontDestroyLibrary(FontLibrary* pLibrary); +int APS5_VABI sceFontCreateRendererWithEdition(const FontMemory* memory, const void* createParams, std::uint64_t edition, FontRenderer* pRenderer); +int APS5_VABI sceFontCreateRenderer(const FontMemory* memory, const void* createParams, FontRenderer* pRenderer); +int APS5_VABI sceFontDestroyRenderer(FontRenderer* pRenderer); +int APS5_VABI sceFontRendererGetOutlineBufferSize(FontRenderer fontRenderer, std::uint32_t* size); +int APS5_VABI sceFontRendererResetOutlineBuffer(FontRenderer fontRenderer); +int APS5_VABI sceFontRendererSetOutlineBufferPolicy(FontRenderer fontRenderer, std::uint64_t bufferPolicy, std::uint32_t basalSize, std::uint32_t limitSize); +int APS5_VABI sceFontGetPixelResolution(FontLibrary library, std::uint32_t* subPixelCount); +int APS5_VABI sceFontAttachDeviceCacheBuffer(FontLibrary library, void* buffer, std::uint32_t size); +int APS5_VABI sceFontClearDeviceCache(FontLibrary library); +int APS5_VABI sceFontDettachDeviceCacheBuffer(FontLibrary library, void** buffer, std::uint32_t* size); +int APS5_VABI sceFontSupportExternalFonts(FontLibrary library, std::uint32_t fontMax, std::uint32_t formats); +int APS5_VABI sceFontSupportSystemFonts(FontLibrary library); +void APS5_VABI sceFontSetGraphicsErrorCallback(Font::GraphicsErrorCallback callback); +int APS5_VABI sceFontSetFontsOpenMode(FontLibrary library, std::uint32_t openMode); +int APS5_VABI sceFontOpenFontMemory(FontLibrary library, const void* fontAddress, std::uint32_t fontSize, const FontOpenDetail* openDetail, FontHandle* pFontHandle); +int APS5_VABI sceFontOpenFontFile(FontLibrary library, const char* path, std::uint32_t openMode, const FontOpenDetail* openDetail, FontHandle* pFontHandle); +int APS5_VABI sceFontOpenFontInstance(FontHandle fontHandle, FontHandle setupFont, FontHandle* pFontHandle); +int APS5_VABI sceFontOpenFontSet(FontLibrary library, std::uint32_t fontSetType, std::uint32_t openMode, const FontOpenDetail* openDetail, FontHandle* pFontHandle); +int APS5_VABI sceFontCloseFont(FontHandle fontHandle); +int APS5_VABI sceFontGetLibrary(FontHandle fontHandle, FontLibrary* pLibrary); +int APS5_VABI sceFontGetFontGlyphsCount(FontHandle fontHandle, std::uint32_t* glyphsCount); +int APS5_VABI sceFontGetCharGlyphCode(FontHandle fontHandle, std::uint32_t code, std::uint32_t* glyphCode); +int APS5_VABI sceFontGetFontResolution(FontHandle fontHandle, std::uint32_t* pResolution, float* pScalePixel); +int APS5_VABI sceFontBindRenderer(FontHandle fontHandle, FontRenderer renderer); +int APS5_VABI sceFontRebindRenderer(FontHandle fontHandle); +int APS5_VABI sceFontUnbindRenderer(FontHandle fontHandle); +int APS5_VABI sceFontGetCharGlyphMetrics(FontHandle fontHandle, std::uint32_t code, FontGlyphMetrics* metrics); +int APS5_VABI sceFontGetRenderCharGlyphMetrics(FontHandle fontHandle, std::uint32_t code, FontGlyphMetrics* metrics); +int APS5_VABI sceFontGetHorizontalLayout(FontHandle fontHandle, FontHorizontalLayout* layout); +int APS5_VABI sceFontGetVerticalLayout(FontHandle fontHandle, FontVerticalLayout* layout); +int APS5_VABI sceFontGetKerning(FontHandle fontHandle, std::uint32_t preCode, std::uint32_t code, FontKerning* kerning); +int APS5_VABI sceFontGetRenderScaledKerning(FontHandle fontHandle, std::uint32_t preCode, std::uint32_t code, FontKerning* kerning); +int APS5_VABI sceFontRenderCharGlyphImage(FontHandle fontHandle, std::uint32_t code, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* result); +int APS5_VABI sceFontRenderCharGlyphImageHorizontal(FontHandle fontHandle, std::uint32_t code, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* result); +int APS5_VABI sceFontRenderCharGlyphImageVertical(FontHandle fontHandle, std::uint32_t code, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* result); +void APS5_VABI sceFontRenderSurfaceInit(FontRenderSurface* renderSurface, void* buffer, int bufWidthByte, int pixelSizeByte, int widthPixel, int heightPixel); +void APS5_VABI sceFontRenderSurfaceSetScissor(FontRenderSurface* renderSurface, int x0, int y0, int w, int h); +int APS5_VABI sceFontRenderSurfaceSetStyleFrame(FontRenderSurface* renderSurface, FontStyleFrame* styleFrame); +int APS5_VABI sceFontGenerateCharGlyph(FontHandle fontHandle, std::uint32_t code, const FontGenerateGlyphDetail* detail, FontGlyph* pGlyph); +int APS5_VABI sceFontDeleteGlyph(const FontMemory* memory, FontGlyph* pGlyph); +int APS5_VABI sceFontGlyphDefineAttribute(FontGlyph glyph, std::int32_t attribute, std::int32_t* previousAttribute); +int APS5_VABI sceFontGlyphGetAttribute(FontGlyph glyph, int attribute, int* nowAttribute); +int APS5_VABI sceFontGlyphGetGlyphForm(FontGlyph glyph); +int APS5_VABI sceFontGlyphGetMetricsForm(FontGlyph glyph); +int APS5_VABI sceFontGlyphGetScalePixel(FontGlyph glyph, float* w, float* h); +const FontGlyphMetrics* APS5_VABI sceFontGlyphRefersMetrics(FontGlyph glyph); +const FontGlyphMetricsHorizontal* APS5_VABI sceFontGlyphRefersMetricsHorizontal(FontGlyph glyph); +const FontGlyphMetricsHorizontalAdvance* APS5_VABI sceFontGlyphRefersMetricsHorizontalAdvance(FontGlyph glyph); +const FontGlyphMetricsHorizontalX* APS5_VABI sceFontGlyphRefersMetricsHorizontalX(FontGlyph glyph); +FontGlyphOutline* APS5_VABI sceFontGlyphRefersOutline(FontGlyph glyph); +int APS5_VABI sceFontDefineAttribute(FontHandle fontHandle, int attribute, int* oldAttribute); +int APS5_VABI sceFontGetAttribute(FontHandle fontHandle, int attribute, int* nowAttribute); +int APS5_VABI sceFontSetScriptLanguage(FontHandle fontHandle, int fontScript, int fontLanguage); +int APS5_VABI sceFontGetScriptLanguage(FontHandle fontHandle, int fontScript, int* fontLanguage); +int APS5_VABI sceFontSetTypographicDesign(FontHandle fontHandle, int typographic, int feature); +int APS5_VABI sceFontGetTypographicDesign(FontHandle fontHandle, int typographic, int* feature); +int APS5_VABI sceFontSetScalePixel(FontHandle fontHandle, float w, float h); +int APS5_VABI sceFontSetScalePoint(FontHandle fontHandle, float w, float h); +int APS5_VABI sceFontSetResolutionDpi(FontHandle fontHandle, std::uint32_t hDpi, std::uint32_t vDpi); +int APS5_VABI sceFontSetEffectSlant(FontHandle fontHandle, float slantRatio); +int APS5_VABI sceFontSetEffectWeight(FontHandle fontHandle, float weightXScale, float weightYScale, std::uint32_t mode); +int APS5_VABI sceFontGetScalePixel(FontHandle fontHandle, float* w, float* h); +int APS5_VABI sceFontGetScalePoint(FontHandle fontHandle, float* w, float* h); +int APS5_VABI sceFontGetResolutionDpi(FontHandle fontHandle, std::uint32_t* hDpi, std::uint32_t* vDpi); +int APS5_VABI sceFontGetEffectSlant(FontHandle fontHandle, float* slantRatio); +int APS5_VABI sceFontGetEffectWeight(FontHandle fontHandle, float* weightXScale, float* weightYScale, std::uint32_t* mode); +int APS5_VABI sceFontSetupRenderScalePixel(FontHandle fontHandle, float w, float h); +int APS5_VABI sceFontSetupRenderScalePoint(FontHandle fontHandle, float w, float h); +int APS5_VABI sceFontSetupRenderEffectSlant(FontHandle fontHandle, float slantRatio); +int APS5_VABI sceFontSetupRenderEffectWeight(FontHandle fontHandle, float weightXScale, float weightYScale, std::uint32_t mode); +int APS5_VABI sceFontGetRenderScalePixel(FontHandle fontHandle, float* w, float* h); +int APS5_VABI sceFontGetRenderScalePoint(FontHandle fontHandle, float* w, float* h); +int APS5_VABI sceFontGetRenderEffectSlant(FontHandle fontHandle, float* slantRatio); +int APS5_VABI sceFontGetRenderEffectWeight(FontHandle fontHandle, float* weightXScale, float* weightYScale, std::uint32_t* mode); +int APS5_VABI sceFontStyleFrameInit(FontStyleFrame* styleFrame); +int APS5_VABI sceFontStyleFrameSetScalePixel(FontStyleFrame* styleFrame, float w, float h); +int APS5_VABI sceFontStyleFrameSetScalePoint(FontStyleFrame* styleFrame, float w, float h); +int APS5_VABI sceFontStyleFrameSetResolutionDpi(FontStyleFrame* styleFrame, std::uint32_t hDpi, std::uint32_t vDpi); +int APS5_VABI sceFontStyleFrameSetEffectSlant(FontStyleFrame* styleFrame, float slantRatio); +int APS5_VABI sceFontStyleFrameSetEffectWeight(FontStyleFrame* styleFrame, float weightXScale, float weightYScale, std::uint32_t mode); +int APS5_VABI sceFontStyleFrameUnsetScale(FontStyleFrame* styleFrame); +int APS5_VABI sceFontStyleFrameUnsetEffectSlant(FontStyleFrame* styleFrame); +int APS5_VABI sceFontStyleFrameUnsetEffectWeight(FontStyleFrame* styleFrame); +int APS5_VABI sceFontStyleFrameGetScalePixel(const FontStyleFrame* styleFrame, float* w, float* h); +int APS5_VABI sceFontStyleFrameGetScalePoint(const FontStyleFrame* styleFrame, float* w, float* h); +int APS5_VABI sceFontStyleFrameGetResolutionDpi(const FontStyleFrame* styleFrame, std::uint32_t* hDpi, std::uint32_t* vDpi); +int APS5_VABI sceFontStyleFrameGetEffectSlant(const FontStyleFrame* styleFrame, float* slantRatio); +int APS5_VABI sceFontStyleFrameGetEffectWeight(const FontStyleFrame* styleFrame, float* weightXScale, float* weightYScale, std::uint32_t* mode); +int APS5_VABI sceFontTextSourceInit(FontTextSource* textSource, const void* textAddress, std::uint32_t textSizeByte, FontTextParseFunction textParser, void* textObject); +int APS5_VABI sceFontTextSourceRewind(FontTextSource* textSource); +int APS5_VABI sceFontTextSourceSetDefaultFont(FontTextSource* textSource, FontHandle defaultFont); +int APS5_VABI sceFontTextSourceSetWritingForm(FontTextSource* textSource, std::int32_t writingForm); +int APS5_VABI sceFontCreateString(const FontMemory* fontMemory, FontTextSource* textSource, const FontCreateStringDetail* detail, FontString* pFontString); +int APS5_VABI sceFontDestroyString(FontString* pFontString); +std::uint32_t APS5_VABI sceFontStringGetTerminateCode(FontString fontString); +void* APS5_VABI sceFontStringGetTerminateOrder(FontString fontString); +int APS5_VABI sceFontStringGetWritingForm(FontString fontString); +FontTextCharacter* APS5_VABI sceFontStringRefersTextCharacters(FontString fontString, std::uint32_t* characterCount); +const FontTextCharacter* APS5_VABI sceFontStringRefersRenderCharacters(FontString fontString, FontTextCharacter* startCharacter, FontTextCharacter* lastCharacter, std::uint32_t* characterCount); +int APS5_VABI sceFontCharacterGetBidiLevel(const FontTextCharacter* textCharacter, int* bidiLevel); +int APS5_VABI sceFontCharacterGetSyllableStringState(const FontTextCharacter* textCharacter, int* syllableStringState); +int APS5_VABI sceFontCharacterGetTextFontCode(const FontTextCharacter* textCharacter, FontHandle* pFontHandle, std::uint32_t* textCode); +int APS5_VABI sceFontCharacterGetTextOrder(const FontTextCharacter* textCharacter, void** pTextOrder); +std::uint32_t APS5_VABI sceFontCharacterLooksFormatCharacters(const FontTextCharacter* textCharacter); +std::uint32_t APS5_VABI sceFontCharacterLooksWhiteSpace(const FontTextCharacter* textCharacter); +FontTextCharacter* APS5_VABI sceFontCharacterRefersTextBack(const FontTextCharacter* textCharacter); +FontTextCharacter* APS5_VABI sceFontCharacterRefersTextNext(const FontTextCharacter* textCharacter); +FontTextCodes* APS5_VABI sceFontCharactersRefersTextCodes(const FontTextCharacter* textCharacter, const FontTextCharacter* termCharacter, FontTextCodes* textCodes); +FontTextCodes* APS5_VABI sceFontTextCodesStepNext(FontTextCodes* textCodesStep); +FontTextCodes* APS5_VABI sceFontTextCodesStepBack(FontTextCodes* textCodesStep); +int APS5_VABI sceFontWritingInit(FontWriting* fontWriting, FontString fontString, const FontTextCharacter* fontCharacter); +int APS5_VABI sceFontWritingGetRenderMetrics(FontWriting* fontWriting, FontWritingMetrics* writingMetrics); +const FontWritingStep* APS5_VABI sceFontWritingRefersRenderStep(FontWriting* fontWriting); +FontTextCharacter* APS5_VABI sceFontWritingRefersRenderStepCharacter(FontWriting* fontWriting, const FontWritingLetterStep** pLetterStep); +int APS5_VABI sceFontWritingSetMaskInvisible(FontWriting* fontWriting, std::int32_t mask); +int APS5_VABI sceFontControl(std::int32_t command, void* object, std::uintptr_t argument); +int APS5_VABI sceFontCreateGraphicsDevice(const FontMemory* memory, const FontGraphicsDeviceParam* param, FontGraphicsDevice** device); +int APS5_VABI sceFontCreateGraphicsService(const FontMemory* memory, const FontGraphicsServiceParam* param, FontGraphicsService** service); +int APS5_VABI sceFontCreateGraphicsServiceWithEdition(const FontMemory* memory, const FontGraphicsServiceParam* param, std::uint64_t unusedSlot, FontGraphicsService** service); +int APS5_VABI sceFontCreateWords(const FontMemory* memory, FontTextSource* source, std::uint64_t unusedSlot, FontWords** words); +int APS5_VABI sceFontDestroyGraphicsDevice(FontGraphicsDevice** device); +int APS5_VABI sceFontDestroyGraphicsService(FontGraphicsService** service); +int APS5_VABI sceFontDestroyWords(FontWords** words); +int APS5_VABI sceFontGetFontGlyphsOutlineProfile(FontHandle font, FontOutlineProfile* profile); +int APS5_VABI sceFontGetFontMetrics(FontHandle font, std::uint32_t mode, FontMetrics* metrics); +int APS5_VABI sceFontGetFontStyleInformation(FontHandle font, FontStyleInformation* information); +int APS5_VABI sceFontGetGlyphExpandBufferState(FontHandle font, FontGlyphExpandBufferState* state); +int APS5_VABI sceFontGlyphRenderImage(FontGlyph glyph, const FontStyleFrame* styleFrame, FontRenderer renderer, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* output); +int APS5_VABI sceFontGlyphRenderImageHorizontal(FontGlyph glyph, const FontStyleFrame* styleFrame, FontRenderer renderer, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* output); +int APS5_VABI sceFontGlyphRenderImageVertical(FontGlyph glyph, const FontStyleFrame* styleFrame, FontRenderer renderer, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* output); +int APS5_VABI sceFontGraphicsBeginFrame(FontGraphicsFrame* frame, std::uint64_t flags, FontGraphicsResource** resource); +int APS5_VABI sceFontGraphicsDrawingCancel(FontGraphicsFrame** frame); +int APS5_VABI sceFontGraphicsDrawingFinish(FontGraphicsFrame** frame, FontGraphicsDevice* device); +int APS5_VABI sceFontGraphicsEndFrame(FontGraphicsFrame* frame); +int APS5_VABI sceFontGraphicsExchangeResource(FontGraphicsResource* resource, FontGraphicsDesignResource* replacement, FontGraphicsDesignResource** previous); +int APS5_VABI sceFontGraphicsFillMethodInit(FontGraphicsFillMethod* method, std::int32_t mode, std::int32_t type); +int APS5_VABI sceFontGraphicsFillPlotInit(FontGraphicsFillPlot* plot, const FontGraphicsRegion* region); +int APS5_VABI sceFontGraphicsFillPlotSetLayout(FontGraphicsFillPlot* plot, std::int32_t first, std::int32_t second); +int APS5_VABI sceFontGraphicsFillPlotSetMapping(FontGraphicsFillPlot* plot, float first, float second, float third, float fourth); +int APS5_VABI sceFontGraphicsFillRatesInit(FontGraphicsFillRates* rates, const FontGraphicsFill* fill, std::uint32_t effect); +int APS5_VABI sceFontGraphicsFillRatesSetFillEffect(FontGraphicsFillRates* rates, const FontGraphicsFill* fill, std::uint32_t effect); +int APS5_VABI sceFontGraphicsFillRatesSetLayout(FontGraphicsFillRates* rates, std::int32_t first, std::int32_t second); +int APS5_VABI sceFontGraphicsFillRatesSetMapping(FontGraphicsFillRates* rates, float first, float second, float third, float fourth); +int APS5_VABI sceFontGraphicsGetDeviceUsage(FontGraphicsDevice* device, FontGraphicsDeviceUsage* usage); +int APS5_VABI sceFontGraphicsRegionInit(FontGraphicsRegion* region, float x, float y, float width, float height); +int APS5_VABI sceFontGraphicsRegionInitCircular(FontGraphicsRegion* region, float centerX, float centerY, float outerRadius, float innerRadius); +int APS5_VABI sceFontGraphicsRegionInitRoundish(FontGraphicsRegion* region, float x, float y, float width, float height, float outerRadius, float innerRadius, std::uint32_t flags); +int APS5_VABI sceFontGraphicsRelease(void** object); +int APS5_VABI sceFontGraphicsRenderResource(FontGraphicsFrame* frame, FontGraphicsDesignResource* design, float x, float y, FontGraphicsResource** resource); +int APS5_VABI sceFontGraphicsSetFramePolicy(FontGraphicsResource* resource, std::int32_t policy, std::uint32_t value); +int APS5_VABI sceFontGraphicsSetupClipping(FontGraphicsResource* resource, const FontGraphicsRegion* region); +int APS5_VABI sceFontGraphicsSetupColorRates(FontGraphicsResource* resource, float red, float green, float blue, float alpha); +int APS5_VABI sceFontGraphicsSetupFillMethod(FontGraphicsResource* resource, const FontGraphicsFillMethod* method); +int APS5_VABI sceFontGraphicsSetupFillRates(FontGraphicsResource* resource, const FontGraphicsFillRates* rates); +int APS5_VABI sceFontGraphicsSetupGlyphFill(FontGraphicsResource* resource, const FontGraphicsFill* fill); +int APS5_VABI sceFontGraphicsSetupGlyphFillPlot(FontGraphicsResource* resource, const FontGraphicsFillPlot* plot); +int APS5_VABI sceFontGraphicsSetupHandleDefault(FontGraphicsResource* resource, std::uint64_t flags); +int APS5_VABI sceFontGraphicsSetupLocation(FontGraphicsResource* resource, float x, float y); +int APS5_VABI sceFontGraphicsSetupPositioning(FontGraphicsResource* resource, std::uint32_t flags, float x, float y); +int APS5_VABI sceFontGraphicsSetupRotation(FontGraphicsResource* resource, float angle, float pivotX, float pivotY); +int APS5_VABI sceFontGraphicsSetupScaling(FontGraphicsResource* resource, float scaleX, float scaleY, float pivotX, float pivotY); +int APS5_VABI sceFontGraphicsSetupShapeFill(FontGraphicsResource* resource, const FontGraphicsFill* fill); +int APS5_VABI sceFontGraphicsSetupShapeFillPlot(FontGraphicsResource* resource, const FontGraphicsFillPlot* plot); +int APS5_VABI sceFontGraphicsStructureCanvas(FontGraphicsDevice* device, std::uint32_t width, std::uint32_t height, std::int32_t format, FontGraphicsCanvas** canvas); +int APS5_VABI sceFontGraphicsStructureCanvasSequence(FontGraphicsDevice* device, FontGraphicsCanvas* canvas, const FontGraphicsSurface* surface, std::uint64_t flags, FontGraphicsCanvasSequence** sequence); +int APS5_VABI sceFontGraphicsStructureDesign(FontGraphicsDevice* device, std::int32_t format, FontGraphicsDesign** design); +int APS5_VABI sceFontGraphicsStructureDesignResource(FontGraphicsDevice* device, FontGraphicsDesign* design, FontGraphicsDesignResource** resource); +int APS5_VABI sceFontGraphicsStructureSurfaceTexture(FontGraphicsDevice* device, const FontGraphicsSurface* surface, FontGraphicsSurfaceTexture** texture); +int APS5_VABI sceFontGraphicsUpdateClipping(FontGraphicsResource* resource, const FontGraphicsRegion* region); +int APS5_VABI sceFontGraphicsUpdateColorRates(FontGraphicsResource* resource, float red, float green, float blue, float alpha); +int APS5_VABI sceFontGraphicsUpdateFillMethod(FontGraphicsResource* resource, const FontGraphicsFillMethod* method); +int APS5_VABI sceFontGraphicsUpdateFillRates(FontGraphicsResource* resource, const FontGraphicsFillRates* rates); +int APS5_VABI sceFontGraphicsUpdateGlyphFill(FontGraphicsResource* resource, const FontGraphicsFill* fill); +int APS5_VABI sceFontGraphicsUpdateGlyphFillPlot(FontGraphicsResource* resource, const FontGraphicsFillPlot* plot); +int APS5_VABI sceFontGraphicsUpdateLocation(FontGraphicsResource* resource, float x, float y); +int APS5_VABI sceFontGraphicsUpdatePositioning(FontGraphicsResource* resource, std::uint32_t flags, float x, float y); +int APS5_VABI sceFontGraphicsUpdateRotation(FontGraphicsResource* resource, float angle, float pivotX, float pivotY); +int APS5_VABI sceFontGraphicsUpdateScaling(FontGraphicsResource* resource, float scaleX, float scaleY, float pivotX, float pivotY); +int APS5_VABI sceFontGraphicsUpdateShapeFill(FontGraphicsResource* resource, const FontGraphicsFill* fill); +int APS5_VABI sceFontGraphicsUpdateShapeFillPlot(FontGraphicsResource* resource, const FontGraphicsFillPlot* plot); +int APS5_VABI sceFontSupportGlyphs(const FontGlyphSupport* support); +int APS5_VABI sceFontWordsFindWordCharacters(FontWords* words, FontTextCharacter* firstCharacter, FontTextCharacter* endCharacter, FontTextCharacter** lastCharacter, FontTextCharacter** nextCharacter); +int APS5_VABI sceFontGraphicsDrawupFillTextureImageObject(FontGraphicsFrame* frame, const void* data, std::uint32_t dataSize, float x, float y, float width, float height, FontGraphicsFill** fill); +int APS5_VABI sceFontGraphicsDrawupFillTexturePatternObject(FontGraphicsFrame* frame, const void* data, std::uint32_t dataSize, std::uint32_t firstMode, std::uint32_t secondMode, std::int32_t firstCount, std::int32_t firstLayout, std::int32_t secondLayout, std::int32_t secondCount, FontGraphicsFill** fill); +int APS5_VABI sceFontCreateWritingLine(const FontMemory* memory, std::int32_t writingForm, const void* writingLineDetail, void** pWritingLine); +int APS5_VABI sceFontDestroyWritingLine(void** pWritingLine); +int APS5_VABI sceFontWritingLineClear(void* writingLine); +int APS5_VABI sceFontWritingLineWritesOrder(void* writingLine, std::uint64_t writingAttribute, const FontWritingMetrics* writingMetrics, void* writingOrderer); +const FontWritingLineStep* APS5_VABI sceFontWritingLineRefersRenderStep(void* writingLine); +int APS5_VABI sceFontWritingLineGetRenderMetrics(void* writingLine, FontWritingMetrics* writingMetrics); +int APS5_VABI sceFontWritingLineGetOrderingSpace(void* writingLine, float* advanceSpace, float* headSpace, float* inlineSpace, float* tailSpace); + +} diff --git a/core/libs/tests/GuestFontGraphics.cpp b/core/libs/tests/GuestFontGraphics.cpp index 7129a32bcf..3093955bc9 100644 --- a/core/libs/tests/GuestFontGraphics.cpp +++ b/core/libs/tests/GuestFontGraphics.cpp @@ -1,5 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" -#include "prx/libSceFont/include/FontGraphics.hpp" +#include "prx/libSceFont/libSceFont.hpp" #include #include #include @@ -7,23 +7,6 @@ #include #include -using GraphicsErrorCallback = void (APS5_VABI*)(int error); - -extern "C" { -void APS5_VABI sceFontSetGraphicsErrorCallback(GraphicsErrorCallback callback); -int APS5_VABI sceFontGraphicsRegionInit(FontGraphicsRegion* region, float x, float y, float width, float height); -int APS5_VABI sceFontGraphicsRegionInitCircular(FontGraphicsRegion* region, float centerX, float centerY, float outerRadius, float innerRadius); -int APS5_VABI sceFontGraphicsRegionInitRoundish(FontGraphicsRegion* region, float x, float y, float width, float height, float outerRadius, float innerRadius, std::uint32_t flags); -int APS5_VABI sceFontGraphicsFillMethodInit(FontGraphicsFillMethod* method, std::int32_t mode, std::int32_t type); -int APS5_VABI sceFontGraphicsFillPlotInit(FontGraphicsFillPlot* plot, const FontGraphicsRegion* region); -int APS5_VABI sceFontGraphicsFillPlotSetLayout(FontGraphicsFillPlot* plot, std::int32_t first, std::int32_t second); -int APS5_VABI sceFontGraphicsFillPlotSetMapping(FontGraphicsFillPlot* plot, float first, float second, float third, float fourth); -int APS5_VABI sceFontGraphicsFillRatesInit(FontGraphicsFillRates* rates, const FontGraphicsFill* fill, std::uint32_t effect); -int APS5_VABI sceFontGraphicsFillRatesSetFillEffect(FontGraphicsFillRates* rates, const FontGraphicsFill* fill, std::uint32_t effect); -int APS5_VABI sceFontGraphicsFillRatesSetLayout(FontGraphicsFillRates* rates, std::int32_t first, std::int32_t second); -int APS5_VABI sceFontGraphicsFillRatesSetMapping(FontGraphicsFillRates* rates, float first, float second, float third, float fourth); -} - namespace { constexpr int InvalidParameter = static_cast(0x80460002); From 83f28bd67fba4190d049c4aa162b77281b440203 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:20:05 +0200 Subject: [PATCH 273/428] fix(libkernel): fail the stat family with EFAULT when the output struct is not writable guest memory fstat, _fstat, sceKernelFstat, stat, lstat, sceKernelStat and _fstatfs filled the caller's struct in place, or copied into it after a check that only tests for null, so a pointer into unmapped memory crashed the host. Fill a local struct and copy it out with a checked write that fails with EFAULT. FreeBSD 11.4 does the same after the lookup has succeeded: copyout(&ub, uap->sb) in sys_fstat (sys/kern/kern_descrip.c:1328), copyout(&sb, uap->ub) in sys_stat (sys/kern/vfs_syscalls.c:2066) and copyout(sfp, uap->buf) in sys_fstatfs (sys/kern/vfs_syscalls.c:337). Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/include/General.hpp | 1 + core/libs/prx/libc/src/General.cpp | 36 ++++++++++++++++++++++ core/libs/prx/libkernel/File/src/Open.cpp | 4 ++- core/libs/prx/libkernel/File/src/Stdio.cpp | 26 +++++++++------- core/libs/tests/GuestFstatFault.cpp | 17 ++++++++++ 5 files changed, 72 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index b45386ab48..88b0941c55 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -29,6 +29,7 @@ extern "C" std::FILE* HostStandardErrorFile_nid_no_patch(); extern "C" bool CopyGuestEnvironment_nid_no_patch(const char* name, std::string& value); extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path); +extern "C" bool GuestCopyOut_nid_no_patch(void* destination, const void* source, std::size_t bytes); extern "C" std::filesystem::path ResolvePathOrRelinkedModule_nid_no_patch(const char* path); extern "C" std::filesystem::path ResolveArchivedPath_nid_no_patch(const char* path, bool module = false); extern "C" std::filesystem::path ResolveDiskPath_nid_no_patch(const char* path); diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 73c1a10082..fac75cc68e 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -30,10 +30,14 @@ #include #include #include +#ifdef __linux__ +#include +#endif #endif #include "prx/libc/include/General.hpp" #include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/AppArchive.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/ApplicationHeap.hpp" namespace { @@ -727,6 +731,38 @@ extern "C" std::filesystem::path ResolveArchivedPath_nid_no_patch(const char* pa return ResolveLocked(path, module, true); } +extern "C" bool GuestCopyOut_nid_no_patch(void* destination, const void* source, std::size_t bytes) { + if (bytes == 0) return true; + const auto start = reinterpret_cast(destination); + if (start == 0 || bytes > UINTPTR_MAX - start) return false; + const GuestArena::HostWrite guard(destination, bytes); + if (!guard.Open()) return false; +#ifdef _WIN32 + for (auto address = start; address < start + bytes;) { + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(reinterpret_cast(address), &memory, sizeof(memory)) != sizeof(memory) || memory.State != MEM_COMMIT || + (memory.Protect & PAGE_GUARD) != 0 || + (memory.Protect & (PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY)) == 0) return false; + address = reinterpret_cast(memory.BaseAddress) + memory.RegionSize; + } + std::memcpy(destination, source, bytes); + return true; +#elif defined(__linux__) + iovec local{const_cast(source), bytes}; + iovec remote{destination, bytes}; + const ssize_t copied = ::process_vm_writev(::getpid(), &local, 1, &remote, 1, 0); + if (copied < 0) { + if (errno == EFAULT) return false; + std::memcpy(destination, source, bytes); + return true; + } + return static_cast(copied) == bytes; +#else + std::memcpy(destination, source, bytes); + return true; +#endif +} + extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path) { auto resolved = ResolveLocked(path, false, true); AppArchiveRefuseOnly_nid_no_patch(resolved, path, "reading"); diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 49bf78e813..bdf4f9b1e9 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -411,7 +411,9 @@ static int StatNative(const std::filesystem::path& native, bool directory, FileS } if (directory && !std::filesystem::is_directory(status)) return SceErrorFromErrno(GuestEnotdir); if (sb == nullptr) return SceErrorFromErrno(GuestEfault); - if (!File::TryFillFileStat(metadataPath, sb)) return SceStatErrorFromErrno(errno); + FileStat result{}; + if (!File::TryFillFileStat(metadataPath, &result)) return SceStatErrorFromErrno(errno); + if (!GuestCopyOut_nid_no_patch(sb, &result, sizeof(result))) return SceErrorFromErrno(GuestEfault); return 0; } diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 338dc6c3a8..5ff636e466 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -888,10 +888,7 @@ int APS5_VABI _fstatfs_nid_postfix(int d, KernelStatfs* buf) { KernelStatfs result{}; result.f_version = KERNEL_STATFS_VERSION; if (const int error = NativeFstatfs(d, &result)) return PosixFailure(error); - if (buf == nullptr) return PosixFailure(GUEST_EFAULT); - const GuestArena::HostWrite destination(buf, sizeof(result)); - if (!destination.Open()) return PosixFailure(GUEST_EFAULT); - std::memcpy(buf, &result, sizeof(result)); + if (!GuestCopyOut_nid_no_patch(buf, &result, sizeof(result))) return PosixFailure(GUEST_EFAULT); return 0; } @@ -901,11 +898,13 @@ int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int d, FileStat* sb) { if (fstat_nid_disambig1_nid_postfix(d, &ignored) != 0) return -1; return PosixFailure(GUEST_EFAULT); } + FileStat result{}; if (GuestSockets::IsOpen(d)) { - if (const int error = GuestSockets::Stat(d, sb)) return PosixFailure(error); - return 0; + if (const int error = GuestSockets::Stat(d, &result)) return PosixFailure(error); + } else if (!File::FillFileStatFromDescriptor(d, &result)) { + return PosixFailure(SceErrorFromErrno(errno) & 0xffff); } - if (!File::FillFileStatFromDescriptor(d, sb)) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); + if (!GuestCopyOut_nid_no_patch(sb, &result, sizeof(result))) return PosixFailure(GUEST_EFAULT); return 0; } @@ -1052,7 +1051,9 @@ int APS5_VABI lstat_nid_postfix(const char* path, FileStat* sb) { if (const int error = PathError(path)) return PosixFailure(error); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); - if (!File::FillLinkStat(native, sb)) return PosixResult(SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno)); + FileStat result{}; + if (!File::FillLinkStat(native, &result)) return PosixResult(SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno)); + if (!GuestCopyOut_nid_no_patch(sb, &result, sizeof(result))) return PosixFailure(GUEST_EFAULT); return 0; } @@ -1078,11 +1079,14 @@ int APS5_VABI sceKernelFstat(int d, FileStat* sb) { if (const int error = sceKernelFstat(d, &ignored)) return error; return SceErrorFromErrno(GUEST_EFAULT); } + FileStat result{}; if (GuestSockets::IsOpen(d)) { - const int error = GuestSockets::Stat(d, sb); - return error == 0 ? 0 : SceErrorFromErrno(error); + const int error = GuestSockets::Stat(d, &result); + if (error != 0) return SceErrorFromErrno(error); + } else if (!File::FillFileStatFromDescriptor(d, &result)) { + return SceErrorFromErrno(errno); } - if (!File::FillFileStatFromDescriptor(d, sb)) return SceErrorFromErrno(errno); + if (!GuestCopyOut_nid_no_patch(sb, &result, sizeof(result))) return SceErrorFromErrno(GUEST_EFAULT); return 0; } diff --git a/core/libs/tests/GuestFstatFault.cpp b/core/libs/tests/GuestFstatFault.cpp index 08ff17cd37..b074d2d732 100644 --- a/core/libs/tests/GuestFstatFault.cpp +++ b/core/libs/tests/GuestFstatFault.cpp @@ -31,6 +31,22 @@ static void RequireFailure(int descriptor, int error) { Require(sceKernelFstat(descriptor, nullptr) == static_cast(0x80020000u | static_cast(error))); } +static void RequireBadPointer(int descriptor, const char* path) { + for (const std::uintptr_t address : {std::uintptr_t{1}, ~std::uintptr_t{0xfff}}) { + auto* bad = reinterpret_cast(address); + *__error_nid_postfix() = 0; + Require(fstat_nid_disambig1_nid_postfix(descriptor, bad) == -1 && *__error_nid_postfix() == GuestEfault); + Require(sceKernelFstat(descriptor, bad) == static_cast(0x80020000u | GuestEfault)); + *__error_nid_postfix() = 0; + Require(stat_nid_postfix(path, bad) == -1 && *__error_nid_postfix() == GuestEfault); + *__error_nid_postfix() = 0; + Require(lstat_nid_postfix(path, bad) == -1 && *__error_nid_postfix() == GuestEfault); + Require(sceKernelStat(path, bad) == static_cast(0x80020000u | GuestEfault)); + *__error_nid_postfix() = 0; + Require(_fstatfs_nid_postfix(descriptor, reinterpret_cast(bad)) == -1 && *__error_nid_postfix() == GuestEfault); + } +} + int main() { const auto root = std::filesystem::temp_directory_path() / ("anyps5-fstat-fault-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); @@ -47,6 +63,7 @@ int main() { FileStat status{}; Require(fstat_nid_disambig1_nid_postfix(file, &status) == 0 && status.st_size == 5); RequireFailure(file, GuestEfault); + RequireBadPointer(file, "/anyps5-fstat-fault-test/file.txt"); int sockets[2]; Require(socketpair_nid_postfix(1, 1, 0, sockets) == 0); From 1f4d1d3d409efff5fd2cf0a059683c0a7274e795 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:31:44 +0000 Subject: [PATCH 274/428] feat(libSceVoice): declare a stub with its shadPS4 signature They still throw through NotImplemented_nid_no_patch; only the parameter lists change. The only source is shadPS4's PS4 prototypes; nothing is measured on a PS5. --- core/libs/prx/libSceVoice/Export.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index a7dfe71792..49b2e7be41 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -447,7 +447,7 @@ int APS5_VABI sceVoiceQAEnableRecording(void) { return 0; } -int APS5_VABI sceVoiceVADAdjustment(void) { +int APS5_VABI sceVoiceVADAdjustment(float param1, float param2) { NotImplemented_nid_no_patch(__func__); return 0; } From dd537b5367bb29bb9dedf2324656b17ec2a9833c Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:31:44 +0000 Subject: [PATCH 275/428] docs(libSceVoice): record the single-source shadPS4 signatures of the declared stubs --- docs/dev/debt/libSceVoice.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceVoice.md b/docs/dev/debt/libSceVoice.md index 3dcdeae2a9..32d3ca5475 100644 --- a/docs/dev/debt/libSceVoice.md +++ b/docs/dev/debt/libSceVoice.md @@ -7,13 +7,13 @@ ### Unknown function info +- [sceVoiceVADAdjustment](../../../core/libs/prx/libSceVoice/Export.cpp) - throws through `NotImplemented_nid_no_patch`; behaviour and the PS5 prototypes are unmeasured. The only source of the parameter lists is shadPS4's PS4-derived prototypes (GPLv2, used as a reference only), with no second reference; structure pointers are `void*` because no layout is known. - [sceVoiceInit, sceVoiceStart, sceVoiceCreatePort, sceVoiceGetPortInfo](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - a second `sceVoiceInit` returns 0x804E0802, and Start, CreatePort and GetPortInfo return 0x804E0801 before checking any argument while the library is not initialized, as [prosper](https://github.com/mattias800/prosper/blob/ed42e499888d5a86792c3e1b2df3aebd8ca1a681/prosper/src/hle/audio/hle_audio.cpp#L4697-L4713) reads them from the shipped module. The other port and lifecycle calls still throw when the library is not initialized, since prosper gives no module address for them. Whether `sceVoiceInit` checks a null param before its state is unknown, so a null param still throws first, and that `sceVoiceEnd` returns the library to the not-initialized state is assumed. Closes when: the other entry points are read from the module or measured on a PS5 - [sceVoiceInitHQ, sceVoiceVADSetVersion](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - not measured on a PS5: the signatures and rules follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/voice/voice.cpp). `sceVoiceInitHQ` runs `sceVoiceInit`, as shadPS4's two share one path. `sceVoiceVADSetVersion` accepts versions 0 and 1 and returns `0x804E0805` for any other; nothing uses the version, as no voice activity detection runs - [sceVoicePausePortAll, sceVoiceResumePortAll](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - not measured on a PS5: both return 0x804E0801 while the library is not initialized and 0 otherwise, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c1b26558352b863f80142256e4df38585aa7d727/src/core/libraries/voice/voice.cpp#L356-L437); prosper reads the same code for `sceVoiceStart`, and the PS3 calls in [RPCS3](https://github.com/RPCS3/rpcs3/blob/be69c054eeac57e2e671697a463473d37fe5a7b2/rpcs3/Emu/Cell/Modules/cellVoice.cpp#L462-L543) make the same check. Like `sceVoicePausePort` they change nothing that is reported: [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L540-L557) documents that pausing every port keeps the buffered data and the reported state, and nothing here processes port data - [sceVoiceGetMemorySize](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceDeviceInCustomerData](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceQAEnableRecording](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature -- [sceVoiceVADAdjustment](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceVADPVTAdjustment](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [sceVoiceSetMuteFlagAll, sceVoiceSetBitRate, sceVoiceUpdatePort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - not measured on a PS5; signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), behaviour the Vita's [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L250-L557). They return the not-initialized, port-range and missing-port codes the other port calls return. The mute-all flag makes `sceVoiceGetMuteFlag` report every port muted without changing port flags. A non-voice port, a bitrate `sceVoiceCreatePort` rejects, a null param or a different port type throw, since the console's codes for them are unknown - [sceVoiceGetPortAttr](../../../core/libs/prx/libSceVoice/Export.cpp): native initialization, port-range, pointer, attribute-size and port-type validation is implemented. Supported device attributes throw because the host has no device state or voice-activity processing. The former constant-zero voice-activity fallback is removed; failed calls leave output untouched. From 509074c26354fd4c46e880558363edfb203440b5 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 23:09:42 +0000 Subject: [PATCH 276/428] fix(libSceVoice): align VAD signature with API header --- core/libs/prx/libSceVoice/Export.cpp | 2 +- core/libs/prx/libSceVoice/include/Api.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index 49b2e7be41..18c75c1db4 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -447,7 +447,7 @@ int APS5_VABI sceVoiceQAEnableRecording(void) { return 0; } -int APS5_VABI sceVoiceVADAdjustment(float param1, float param2) { +int APS5_VABI sceVoiceVADAdjustment(float, float) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceVoice/include/Api.hpp b/core/libs/prx/libSceVoice/include/Api.hpp index cdaf1823b6..4ac2972a27 100644 --- a/core/libs/prx/libSceVoice/include/Api.hpp +++ b/core/libs/prx/libSceVoice/include/Api.hpp @@ -32,7 +32,7 @@ int APS5_VABI sceVoiceInitHQ(const VoiceInitParam* param, std::int32_t version); int APS5_VABI sceVoiceGetMemorySize(void); int APS5_VABI sceVoiceDeviceInCustomerData(void); int APS5_VABI sceVoiceQAEnableRecording(void); -int APS5_VABI sceVoiceVADAdjustment(void); +int APS5_VABI sceVoiceVADAdjustment(float param1, float param2); int APS5_VABI sceVoiceVADPVTAdjustment(void); int APS5_VABI sceVoiceVADSetVersion(std::int32_t version); int APS5_VABI sceVoiceSetMuteFlagAll(bool muted); From da3e88a21d137ee000b77157943ce4b62f133d56 Mon Sep 17 00:00:00 2001 From: Alexandre Figueiredo Date: Sun, 25 Oct 2026 00:10:14 +0100 Subject: [PATCH 277/428] feat(libSceNpCommon): forward sceNpCloseEventFlag to sceKernelCloseEventFlag --- .../libs/prx/libSceNpCommon/PendingExports.cpp | 5 ----- core/libs/prx/libSceNpCommon/Runtime.cpp | 4 ++++ core/libs/prx/libSceNpCommon/include/Api.hpp | 2 +- core/libs/tests/GuestNpCommonRuntime.cpp | 18 ++++++++++++++++++ docs/dev/debt/libSceNpCommon.md | 5 ++--- 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceNpCommon/PendingExports.cpp b/core/libs/prx/libSceNpCommon/PendingExports.cpp index 7c3808ebf3..b6479f111b 100644 --- a/core/libs/prx/libSceNpCommon/PendingExports.cpp +++ b/core/libs/prx/libSceNpCommon/PendingExports.cpp @@ -6,11 +6,6 @@ extern "C" { -int APS5_VABI sceNpCloseEventFlag(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpCloseSema(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceNpCommon/Runtime.cpp b/core/libs/prx/libSceNpCommon/Runtime.cpp index 8c8edda9b4..510328687f 100644 --- a/core/libs/prx/libSceNpCommon/Runtime.cpp +++ b/core/libs/prx/libSceNpCommon/Runtime.cpp @@ -326,6 +326,10 @@ int APS5_VABI sceNpOpenEventFlag(KernelEventFlag* flag, const char* name) { return NpResult(sceKernelOpenEventFlag(flag, name)); } +int APS5_VABI sceNpCloseEventFlag(KernelEventFlag flag) { + return NpResult(sceKernelCloseEventFlag(flag)); +} + int APS5_VABI sceNpSetEventFlag(KernelEventFlag flag, std::uint64_t pattern) { return NpResult(sceKernelSetEventFlag(flag, pattern)); } diff --git a/core/libs/prx/libSceNpCommon/include/Api.hpp b/core/libs/prx/libSceNpCommon/include/Api.hpp index 6e1bf26bc7..d4c373e182 100644 --- a/core/libs/prx/libSceNpCommon/include/Api.hpp +++ b/core/libs/prx/libSceNpCommon/include/Api.hpp @@ -87,7 +87,6 @@ int APS5_VABI sceNpCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, std::ui int APS5_VABI sceNpJoinThread(Pthread thread, void** retval); int APS5_VABI sceNpGetSystemClockUsec(std::int64_t* usec); void APS5_VABI sceNpGetSdkVersion(char* version); -int APS5_VABI sceNpCloseEventFlag(void); int APS5_VABI sceNpCloseSema(void); int APS5_VABI sceNpGetNavSdkVersion(void); int APS5_VABI sceNpIntGetPlatformType(void); @@ -137,6 +136,7 @@ int APS5_VABI sceNpLwCondWait(PthreadCond* cond, PthreadMutex* mutex); int APS5_VABI sceNpCreateEventFlag(KernelEventFlag* flag, const char* name, std::uint32_t attr, std::uint64_t pattern); int APS5_VABI sceNpDeleteEventFlag(KernelEventFlag flag); int APS5_VABI sceNpOpenEventFlag(KernelEventFlag* flag, const char* name); +int APS5_VABI sceNpCloseEventFlag(KernelEventFlag flag); int APS5_VABI sceNpSetEventFlag(KernelEventFlag flag, std::uint64_t pattern); int APS5_VABI sceNpClearEventFlag(KernelEventFlag flag, std::uint64_t pattern); int APS5_VABI sceNpPollEventFlag(KernelEventFlag flag, std::uint64_t pattern, std::uint32_t mode, std::uint64_t* result); diff --git a/core/libs/tests/GuestNpCommonRuntime.cpp b/core/libs/tests/GuestNpCommonRuntime.cpp index 7b2ad35b4c..1747776629 100644 --- a/core/libs/tests/GuestNpCommonRuntime.cpp +++ b/core/libs/tests/GuestNpCommonRuntime.cpp @@ -30,7 +30,10 @@ constexpr int NpOutOfRange = static_cast(0x8055800E); constexpr int NpInvalidArgument = static_cast(0x80558009); constexpr int NpEventFlagBusy = static_cast(0x8055800C); constexpr int NpSemaBusy = static_cast(0x8055800D); +constexpr int KernelNotPermitted = static_cast(0x80020001); +constexpr int KernelNoSuchObject = static_cast(0x80020003); constexpr std::uint32_t EventFlagWaitAnd = 1; +constexpr std::uint32_t EventFlagShared = 0x100; void Require(bool value, const char* message) { if (value) return; @@ -67,6 +70,20 @@ void TestSemaphoresAndFlags() { Require(sceNpDeleteEventFlag(flag) == 0, "the event flag is deleted"); } +void TestSharedEventFlags() { + KernelEventFlag creator {}; + Require(sceNpCreateEventFlag(&creator, "NpSharedFlag", EventFlagShared, 0) == 0, "a shared event flag is created"); + KernelEventFlag opened {}; + Require(sceNpOpenEventFlag(&opened, "NpSharedFlag") == 0 && opened != creator, "the shared event flag is opened by name"); + std::uint64_t result = 0; + Require(sceNpSetEventFlag(opened, 2) == 0 && sceNpPollEventFlag(creator, 2, EventFlagWaitAnd, &result) == 0 && result == 2, + "a bit set through the opened handle is polled through the creator"); + Require(sceNpCloseEventFlag(creator) == KernelNotPermitted, "the creator handle is not closed"); + Require(sceNpCloseEventFlag(opened) == 0, "the opened handle is closed"); + Require(sceNpCloseEventFlag(opened) == KernelNoSuchObject, "a closed handle is gone"); + Require(sceNpDeleteEventFlag(creator) == 0, "the creator deletes the shared event flag"); +} + void TestThreads() { Require(sceNpThreadGetId(nullptr) == NpInvalidArgument, "a null thread id output is refused"); Pthread thread {}; @@ -400,6 +417,7 @@ void TestHexAndCopies() { int main() { TestAtomics(); TestSemaphoresAndFlags(); + TestSharedEventFlags(); TestThreads(); TestHeaps(); TestBase64(); diff --git a/docs/dev/debt/libSceNpCommon.md b/docs/dev/debt/libSceNpCommon.md index da67369967..be7ac29c21 100644 --- a/docs/dev/debt/libSceNpCommon.md +++ b/docs/dev/debt/libSceNpCommon.md @@ -2,9 +2,8 @@ ### Unknown function info -The 35 declarations below use names mapped from the local `libSceNpCommon.sprx` export table. Each NID was verified against that table and recomputed from its name with `tools/nid_names.py`. The public [PS5 3.20 libSceNpCommon export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/main/libSceNpCommon.c) contains all 150 symbol names. No prototype or behavior is claimed. Each declaration uses the no-parameter unknown-signature convention solely for import resolution and throws if called. +The 34 declarations below use names mapped from the local `libSceNpCommon.sprx` export table. Each NID was verified against that table and recomputed from its name with `tools/nid_names.py`. The public [PS5 3.20 libSceNpCommon export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/main/libSceNpCommon.c) contains all 150 symbol names. No prototype or behavior is claimed. Each declaration uses the no-parameter unknown-signature convention solely for import resolution and throws if called. -- [sceNpCloseEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `+nmn+Z0nWDo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCloseSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `8hPzfjZzV88`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGetNavSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `QmDEFikd3VA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIntGetPlatformType](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `G6qytFoBJ-w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. @@ -54,6 +53,6 @@ The 35 declarations below use names mapped from the local `libSceNpCommon.sprx` - [The string-to-integer exports](../../../core/libs/prx/libSceNpCommon/Strings.cpp) clear `errno` before parsing; the firmware reads whatever `errno` held before the call when the result saturates. - [The integer-to-string exports](../../../core/libs/prx/libSceNpCommon/Strings.cpp) return `0x80558008` when the hexadecimal prefix does not fit; the firmware writes the prefix without checking the size. [The base64 decoders](../../../core/libs/prx/libSceNpCommon/Strings.cpp) treat bytes above 0x7F as invalid; the firmware indexes its table with them as signed values. [sceNpUtilJsonGetOneChar](../../../core/libs/prx/libSceNpCommon/Strings.cpp) checks that a `\u` escape and a surrogate pair fit in the input; the firmware reads past it. - [sceNpUtilGetNpDebug, sceNpUtilGetNpTestPatch, sceNpUtilGetTrcNotify, sceNpUtilGetIgnoreNpTitleId, sceNpUtilGetFakeDisplayNameMode, sceNpUtilGetWebTraceSetting, sceNpUtilGetWebApi2FakeRateLimit, sceNpUtilGetWebApi2FakeRateLimitTarget, sceNpUtilGetFakeRateLimit, sceNpUtilGetDbgCommerce, sceNpUtilGetSystemLanguage, sceNpUtilGetEnv, sceNpUtilGetDateSetAuto and sceNpUtilGetShareTitleCheck](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) read system and debug registry keys through libSceRegMgr, whose `sceRegMgrNonSysGetInt` and string reads still throw; they throw until those exist. -- [sceNpOpenSema, sceNpCloseSema and sceNpCloseEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) forward to the libkernel open and close calls for named semaphores and event flags, which still throw. +- [sceNpOpenSema and sceNpCloseSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) forward to the libkernel open and close calls for named semaphores, which still throw. - [sceNpSetPlatformType, sceNpIntGetPlatformType, sceNpIntSetPlatformType, sceNpUtilCanonicalizeNpIdForPs4, sceNpUtilCanonicalizeNpIdForPsp2, sceNpUtilJidToNpId and sceNpGetNavSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) depend on console platform tags, network domain names or a version string of the console; they throw. - [sceNpJsonParse, sceNpJsonParseInit, sceNpJsonParseBuf, sceNpJsonParseBufInit, sceNpJsonParseEx, sceNpJsonParseExInit and the sceNpServerErrorJson exports](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) are a streaming, schema-checked JSON parser whose event and error rules are not yet specified; they throw. From c424bd17c83045b075eb665093712ea18b2eab21 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Fri, 23 Oct 2026 03:08:38 +0200 Subject: [PATCH 278/428] fix(libc): never fall back to an unchecked copy-out on non-EFAULT host errors GuestCopyOut_nid_no_patch fell back to memcpy when process_vm_writev failed with anything but EFAULT, and on every non-Linux/non-Windows host. Throw instead (macOS uses mach_vm_write as Udp.cpp does) and cover read-only and PROT_NONE pages plus a seccomp EPERM child in guest_fstat_fault. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/src/General.cpp | 12 +++-- core/libs/tests/GuestFstatFault.cpp | 80 +++++++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index fac75cc68e..4e6cb2f8b9 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -32,6 +32,9 @@ #include #ifdef __linux__ #include +#elif defined(__APPLE__) +#include +#include #endif #endif #include "prx/libc/include/General.hpp" @@ -753,13 +756,14 @@ extern "C" bool GuestCopyOut_nid_no_patch(void* destination, const void* source, const ssize_t copied = ::process_vm_writev(::getpid(), &local, 1, &remote, 1, 0); if (copied < 0) { if (errno == EFAULT) return false; - std::memcpy(destination, source, bytes); - return true; + throw std::runtime_error("GuestCopyOut: process_vm_writev failed with errno " + std::to_string(errno)); } return static_cast(copied) == bytes; +#elif defined(__APPLE__) + return ::mach_vm_write(::mach_task_self(), reinterpret_cast(destination), + reinterpret_cast(source), static_cast(bytes)) == KERN_SUCCESS; #else - std::memcpy(destination, source, bytes); - return true; + throw std::runtime_error("GuestCopyOut: no checked write is available on this host"); #endif } diff --git a/core/libs/tests/GuestFstatFault.cpp b/core/libs/tests/GuestFstatFault.cpp index b074d2d732..669cb3c903 100644 --- a/core/libs/tests/GuestFstatFault.cpp +++ b/core/libs/tests/GuestFstatFault.cpp @@ -6,13 +6,30 @@ #include "prx/libkernel/File/include/FileFlags.hpp" #include +#include #include #include #include #include #include +#include #include +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif +#ifdef __linux__ +#include +#include +#include +#include +#include +#endif + static void Check(bool value, int line) { if (!value) { std::fprintf(stderr, "fstat check failed at line %d\n", line); @@ -31,9 +48,9 @@ static void RequireFailure(int descriptor, int error) { Require(sceKernelFstat(descriptor, nullptr) == static_cast(0x80020000u | static_cast(error))); } -static void RequireBadPointer(int descriptor, const char* path) { - for (const std::uintptr_t address : {std::uintptr_t{1}, ~std::uintptr_t{0xfff}}) { - auto* bad = reinterpret_cast(address); +static void RequireBadPointerAt(int descriptor, const char* path, void* address) { + { + auto* bad = static_cast(address); *__error_nid_postfix() = 0; Require(fstat_nid_disambig1_nid_postfix(descriptor, bad) == -1 && *__error_nid_postfix() == GuestEfault); Require(sceKernelFstat(descriptor, bad) == static_cast(0x80020000u | GuestEfault)); @@ -47,6 +64,56 @@ static void RequireBadPointer(int descriptor, const char* path) { } } +static void RequireUnwritablePage(int descriptor, const char* path) { +#ifdef _WIN32 + void* page = VirtualAlloc(nullptr, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_READONLY); + Require(page != nullptr); + void* none = VirtualAlloc(nullptr, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_NOACCESS); + Require(none != nullptr); + RequireBadPointerAt(descriptor, path, page); + RequireBadPointerAt(descriptor, path, none); + VirtualFree(page, 0, MEM_RELEASE); + VirtualFree(none, 0, MEM_RELEASE); +#else + void* page = mmap(nullptr, 4096, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(page != MAP_FAILED); + void* none = mmap(nullptr, 4096, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(none != MAP_FAILED); + RequireBadPointerAt(descriptor, path, page); + RequireBadPointerAt(descriptor, path, none); + munmap(page, 4096); + munmap(none, 4096); +#endif +} + +#ifdef __linux__ +static void RequireNonEfaultFailureThrows() { + const pid_t child = fork(); + Require(child >= 0); + if (child == 0) { + sock_filter filter[] = { + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_process_vm_writev, 0, 1), + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM), + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW), + }; + sock_fprog program{static_cast(sizeof(filter) / sizeof(filter[0])), filter}; + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0 || prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &program) != 0) _exit(2); + char source[8] = {1, 2, 3, 4, 5, 6, 7, 8}; + char destination[8] = {}; + try { + GuestCopyOut_nid_no_patch(destination, source, sizeof(source)); + } catch (const std::runtime_error&) { + _exit(destination[0] == 0 ? 0 : 3); + } + _exit(1); + } + int status = 0; + Require(waitpid(child, &status, 0) == child); + Require(WIFEXITED(status) && WEXITSTATUS(status) == 0); +} +#endif + int main() { const auto root = std::filesystem::temp_directory_path() / ("anyps5-fstat-fault-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); @@ -63,7 +130,12 @@ int main() { FileStat status{}; Require(fstat_nid_disambig1_nid_postfix(file, &status) == 0 && status.st_size == 5); RequireFailure(file, GuestEfault); - RequireBadPointer(file, "/anyps5-fstat-fault-test/file.txt"); + for (const std::uintptr_t address : {std::uintptr_t{1}, ~std::uintptr_t{0xfff}}) + RequireBadPointerAt(file, "/anyps5-fstat-fault-test/file.txt", reinterpret_cast(address)); + RequireUnwritablePage(file, "/anyps5-fstat-fault-test/file.txt"); +#ifdef __linux__ + RequireNonEfaultFailureThrows(); +#endif int sockets[2]; Require(socketpair_nid_postfix(1, 1, 0, sockets) == 0); From fabf1a11b50baee95ddd5cc1b42d43d9893a26e4 Mon Sep 17 00:00:00 2001 From: hypermist Date: Sun, 25 Oct 2026 12:09:24 +1300 Subject: [PATCH 279/428] refactor(libSceSpSysCallWrapper): declare wrapper signatures in the library header --- .../prx/libSceSpSysCallWrapper/Export.cpp | 51 +--------- .../libSceSpSysCallWrapper/include/Api.hpp | 92 ++++++++++--------- core/libs/tests/GuestSpSysCallWrapper.cpp | 68 +------------- 3 files changed, 55 insertions(+), 156 deletions(-) diff --git a/core/libs/prx/libSceSpSysCallWrapper/Export.cpp b/core/libs/prx/libSceSpSysCallWrapper/Export.cpp index bf7ce4f49e..840fdb839e 100644 --- a/core/libs/prx/libSceSpSysCallWrapper/Export.cpp +++ b/core/libs/prx/libSceSpSysCallWrapper/Export.cpp @@ -1,4 +1,6 @@ -#include "prx/libSceSpSysCallWrapper/include/Api.hpp" +#include "prx/libSceSpSysCallWrapper/libSceSpSysCallWrapper.hpp" +#include "prx/libSceNet/libSceNet.hpp" +#include "prx/libkernel/libkernel.hpp" #include #include #include "SceTypes.hpp" @@ -8,49 +10,6 @@ extern "C" { std::int64_t APS5_VABI GuestNetRecv_nid_no_patch(int s, void* buf, std::size_t len, int flags); std::int64_t APS5_VABI GuestNetSend_nid_no_patch(int s, const void* buf, std::size_t len, int flags); -int APS5_VABI sceKernelGettimeofday(KernelTimeval* tp); -int APS5_VABI sceKernelNanosleep(const KernelTimespec* rqtp, KernelTimespec* rmtp); -int APS5_VABI sceNetConnect(int s, const void* addr, std::uint32_t addrlen); -int APS5_VABI sceNetEpollAbort(int eid, int flags); -int APS5_VABI sceNetEpollControl(int eid, int op, int id, const void* event); -int APS5_VABI sceNetEpollCreate(const char* name, int flags); -int APS5_VABI sceNetEpollDestroy(int eid); -int APS5_VABI sceNetEpollWait(int eid, void* events, int maxevents, int timeout); -int* APS5_VABI sceNetErrnoLoc(void); -int APS5_VABI sceNetPoolCreate(const char* name, int size, int flags); -int APS5_VABI sceNetPoolDestroy(int memid); -int APS5_VABI sceNetResolverAbort(int rid, int flags); -int APS5_VABI sceNetResolverCreate(const char* name, int memid, int flags); -int APS5_VABI sceNetResolverDestroy(int rid); -int APS5_VABI sceNetResolverGetError(int rid, int* status); -int APS5_VABI sceNetResolverStartNtoa(int rid, const char* hostname, void* addr, int timeout, int retry, int flags); -int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval, std::uint32_t optlen); -int APS5_VABI sceNetSocket(const char* name, int family, int type, int protocol); -int APS5_VABI sceNetSocketClose(int s); -int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); -int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); -int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr* attr, int inheritSched); -int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param); -int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stackSize); -int APS5_VABI scePthreadCondDestroy(PthreadCond* cond); -int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, const char* name); -int APS5_VABI scePthreadCondSignal(PthreadCond* cond); -int APS5_VABI scePthreadCondWait(PthreadCond* cond, PthreadMutex* mutex); -int APS5_VABI scePthreadCondattrDestroy(PthreadCondattr* attr); -int APS5_VABI scePthreadCondattrInit(PthreadCondattr* attr); -int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); -int APS5_VABI scePthreadDetach(Pthread thread); -void APS5_VABI scePthreadExit(void* retval); -int APS5_VABI scePthreadJoin(Pthread thread, void** retval); -int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); -int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char* name); -int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); -int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); -int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr); -int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr* attr); -int APS5_VABI scePthreadMutexattrSetprotocol(PthreadMutexattr* attr, int protocol); -int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type); -int APS5_VABI scePthreadSetprio(Pthread thread, int prio); int APS5_VABI sceSpGetPid() { NotImplemented_nid_no_patch(__func__); @@ -74,7 +33,7 @@ int APS5_VABI sceSpNetEpollAbort(int eid, int flags) { } int APS5_VABI sceSpNetEpollControl(int eid, int op, int id, const void* event) { - return sceNetEpollControl(eid, op, id, event); + return sceNetEpollControl(eid, op, id, static_cast(event)); } int APS5_VABI sceSpNetEpollCreate(const char* name, int flags) { @@ -86,7 +45,7 @@ int APS5_VABI sceSpNetEpollDestroy(int eid) { } int APS5_VABI sceSpNetEpollWait(int eid, void* events, int maxevents, int timeout) { - return sceNetEpollWait(eid, events, maxevents, timeout); + return sceNetEpollWait(eid, static_cast(events), maxevents, timeout); } int* APS5_VABI sceSpNetErrnoLoc() { diff --git a/core/libs/prx/libSceSpSysCallWrapper/include/Api.hpp b/core/libs/prx/libSceSpSysCallWrapper/include/Api.hpp index 86136aef04..f53f02a76b 100644 --- a/core/libs/prx/libSceSpSysCallWrapper/include/Api.hpp +++ b/core/libs/prx/libSceSpSysCallWrapper/include/Api.hpp @@ -1,56 +1,58 @@ #pragma once +#include +#include #include "SceTypes.hpp" extern "C" { int APS5_VABI sceSpGetPid(); -int APS5_VABI sceSpKernelGettimeofday(); -int APS5_VABI sceSpKernelNanosleep(); -int APS5_VABI sceSpNetConnect(); -int APS5_VABI sceSpNetEpollAbort(); -int APS5_VABI sceSpNetEpollControl(); -int APS5_VABI sceSpNetEpollCreate(); -int APS5_VABI sceSpNetEpollDestroy(); -int APS5_VABI sceSpNetEpollWait(); -int APS5_VABI sceSpNetErrnoLoc(); -int APS5_VABI sceSpNetPoolCreate(); -int APS5_VABI sceSpNetPoolDestroy(); -int APS5_VABI sceSpNetRecv(); -int APS5_VABI sceSpNetResolverAbort(); -int APS5_VABI sceSpNetResolverCreate(); -int APS5_VABI sceSpNetResolverDestroy(); -int APS5_VABI sceSpNetResolverGetError(); -int APS5_VABI sceSpNetResolverStartNtoa(); -int APS5_VABI sceSpNetSend(); -int APS5_VABI sceSpNetSetsockopt(); -int APS5_VABI sceSpNetSocket(); -int APS5_VABI sceSpNetSocketClose(); -int APS5_VABI sceSpPthreadAttrDestroy(); -int APS5_VABI sceSpPthreadAttrInit(); -int APS5_VABI sceSpPthreadAttrSetinheritsched(); -int APS5_VABI sceSpPthreadAttrSetschedparam(); -int APS5_VABI sceSpPthreadAttrSetstacksize(); -int APS5_VABI sceSpPthreadCondDestroy(); -int APS5_VABI sceSpPthreadCondInit(); -int APS5_VABI sceSpPthreadCondSignal(); -int APS5_VABI sceSpPthreadCondWait(); -int APS5_VABI sceSpPthreadCondattrDestroy(); -int APS5_VABI sceSpPthreadCondattrInit(); -int APS5_VABI sceSpPthreadCreate(); -int APS5_VABI sceSpPthreadDetach(); -int APS5_VABI sceSpPthreadExit(); -int APS5_VABI sceSpPthreadJoin(); -int APS5_VABI sceSpPthreadMutexDestroy(); -int APS5_VABI sceSpPthreadMutexInit(); -int APS5_VABI sceSpPthreadMutexLock(); -int APS5_VABI sceSpPthreadMutexUnlock(); -int APS5_VABI sceSpPthreadMutexattrDestroy(); -int APS5_VABI sceSpPthreadMutexattrInit(); +int APS5_VABI sceSpKernelGettimeofday(KernelTimeval* tp); +int APS5_VABI sceSpKernelNanosleep(const KernelTimespec* rqtp, KernelTimespec* rmtp); +int APS5_VABI sceSpNetConnect(int s, const void* addr, std::uint32_t addrlen); +int APS5_VABI sceSpNetEpollAbort(int eid, int flags); +int APS5_VABI sceSpNetEpollControl(int eid, int op, int id, const void* event); +int APS5_VABI sceSpNetEpollCreate(const char* name, int flags); +int APS5_VABI sceSpNetEpollDestroy(int eid); +int APS5_VABI sceSpNetEpollWait(int eid, void* events, int maxevents, int timeout); +int* APS5_VABI sceSpNetErrnoLoc(); +int APS5_VABI sceSpNetPoolCreate(const char* name, int size, int flags); +int APS5_VABI sceSpNetPoolDestroy(int memid); +std::int64_t APS5_VABI sceSpNetRecv(int s, void* buf, std::size_t len, int flags); +int APS5_VABI sceSpNetResolverAbort(int rid, int flags); +int APS5_VABI sceSpNetResolverCreate(const char* name, int memid, int flags); +int APS5_VABI sceSpNetResolverDestroy(int rid); +int APS5_VABI sceSpNetResolverGetError(int rid, int* status); +int APS5_VABI sceSpNetResolverStartNtoa(int rid, const char* hostname, void* addr, int timeout, int retry, int flags); +std::int64_t APS5_VABI sceSpNetSend(int s, const void* buf, std::size_t len, int flags); +int APS5_VABI sceSpNetSetsockopt(int s, int level, int optname, const void* optval, std::uint32_t optlen); +int APS5_VABI sceSpNetSocket(const char* name, int family, int type, int protocol); +int APS5_VABI sceSpNetSocketClose(int s); +int APS5_VABI sceSpPthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI sceSpPthreadAttrInit(PthreadAttr* attr); +int APS5_VABI sceSpPthreadAttrSetinheritsched(PthreadAttr* attr, int inheritSched); +int APS5_VABI sceSpPthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param); +int APS5_VABI sceSpPthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stackSize); +int APS5_VABI sceSpPthreadCondDestroy(PthreadCond* cond); +int APS5_VABI sceSpPthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, const char* name); +int APS5_VABI sceSpPthreadCondSignal(PthreadCond* cond); +int APS5_VABI sceSpPthreadCondWait(PthreadCond* cond, PthreadMutex* mutex); +int APS5_VABI sceSpPthreadCondattrDestroy(PthreadCondattr* attr); +int APS5_VABI sceSpPthreadCondattrInit(PthreadCondattr* attr); +int APS5_VABI sceSpPthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI sceSpPthreadDetach(Pthread thread); +void APS5_VABI sceSpPthreadExit(void* retval); +int APS5_VABI sceSpPthreadJoin(Pthread thread, void** retval); +int APS5_VABI sceSpPthreadMutexDestroy(PthreadMutex* mutex); +int APS5_VABI sceSpPthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char* name); +int APS5_VABI sceSpPthreadMutexLock(PthreadMutex* mutex); +int APS5_VABI sceSpPthreadMutexUnlock(PthreadMutex* mutex); +int APS5_VABI sceSpPthreadMutexattrDestroy(PthreadMutexattr* attr); +int APS5_VABI sceSpPthreadMutexattrInit(PthreadMutexattr* attr); int APS5_VABI sceSpPthreadMutexattrSetprioceiling(); -int APS5_VABI sceSpPthreadMutexattrSetprotocol(); -int APS5_VABI sceSpPthreadMutexattrSettype(); -int APS5_VABI sceSpPthreadSetprio(); +int APS5_VABI sceSpPthreadMutexattrSetprotocol(PthreadMutexattr* attr, int protocol); +int APS5_VABI sceSpPthreadMutexattrSettype(PthreadMutexattr* attr, int type); +int APS5_VABI sceSpPthreadSetprio(Pthread thread, int prio); int APS5_VABI sceSpSelect(); } diff --git a/core/libs/tests/GuestSpSysCallWrapper.cpp b/core/libs/tests/GuestSpSysCallWrapper.cpp index 80bce86a0d..6b51574efb 100644 --- a/core/libs/tests/GuestSpSysCallWrapper.cpp +++ b/core/libs/tests/GuestSpSysCallWrapper.cpp @@ -1,73 +1,11 @@ #include "SceTypes.hpp" +#include "prx/libSceSpSysCallWrapper/libSceSpSysCallWrapper.hpp" +#include "prx/libSceNet/libSceNet.hpp" +#include "prx/libkernel/libkernel.hpp" #include #include #include -extern "C" { -int APS5_VABI sceSpKernelGettimeofday(KernelTimeval* tp); -int APS5_VABI sceSpKernelNanosleep(const KernelTimespec* rqtp, KernelTimespec* rmtp); -int APS5_VABI sceSpNetConnect(int s, const void* addr, std::uint32_t addrlen); -int APS5_VABI sceSpNetEpollAbort(int eid, int flags); -int APS5_VABI sceSpNetEpollControl(int eid, int op, int id, const void* event); -int APS5_VABI sceSpNetEpollCreate(const char* name, int flags); -int APS5_VABI sceSpNetEpollDestroy(int eid); -int APS5_VABI sceSpNetEpollWait(int eid, void* events, int maxevents, int timeout); -int* APS5_VABI sceSpNetErrnoLoc(); -int APS5_VABI sceSpNetPoolCreate(const char* name, int size, int flags); -int APS5_VABI sceSpNetPoolDestroy(int memid); -std::int64_t APS5_VABI sceSpNetRecv(int s, void* buf, std::size_t len, int flags); -int APS5_VABI sceSpNetResolverAbort(int rid, int flags); -int APS5_VABI sceSpNetResolverCreate(const char* name, int memid, int flags); -int APS5_VABI sceSpNetResolverDestroy(int rid); -int APS5_VABI sceSpNetResolverGetError(int rid, int* status); -std::int64_t APS5_VABI sceSpNetSend(int s, const void* buf, std::size_t len, int flags); -int APS5_VABI sceSpNetSetsockopt(int s, int level, int optname, const void* optval, std::uint32_t optlen); -int APS5_VABI sceSpNetSocket(const char* name, int family, int type, int protocol); -int APS5_VABI sceSpNetSocketClose(int s); -int APS5_VABI sceSpPthreadAttrDestroy(PthreadAttr* attr); -int APS5_VABI sceSpPthreadAttrInit(PthreadAttr* attr); -int APS5_VABI sceSpPthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stackSize); -int APS5_VABI sceSpPthreadCondDestroy(PthreadCond* cond); -int APS5_VABI sceSpPthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, const char* name); -int APS5_VABI sceSpPthreadCondSignal(PthreadCond* cond); -int APS5_VABI sceSpPthreadCondWait(PthreadCond* cond, PthreadMutex* mutex); -int APS5_VABI sceSpPthreadCondattrDestroy(PthreadCondattr* attr); -int APS5_VABI sceSpPthreadCondattrInit(PthreadCondattr* attr); -int APS5_VABI sceSpPthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); -int APS5_VABI sceSpPthreadDetach(Pthread thread); -void APS5_VABI sceSpPthreadExit(void* retval); -int APS5_VABI sceSpPthreadJoin(Pthread thread, void** retval); -int APS5_VABI sceSpPthreadMutexDestroy(PthreadMutex* mutex); -int APS5_VABI sceSpPthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char* name); -int APS5_VABI sceSpPthreadMutexLock(PthreadMutex* mutex); -int APS5_VABI sceSpPthreadMutexUnlock(PthreadMutex* mutex); -int APS5_VABI sceSpPthreadMutexattrDestroy(PthreadMutexattr* attr); -int APS5_VABI sceSpPthreadMutexattrInit(PthreadMutexattr* attr); -int APS5_VABI sceSpPthreadMutexattrSettype(PthreadMutexattr* attr, int type); - -int* APS5_VABI sceNetErrnoLoc(); -int APS5_VABI sceNetConnect(int s, const void* addr, std::uint32_t addrlen); -int APS5_VABI sceNetEpollAbort(int eid, int flags); -int APS5_VABI sceNetEpollControl(int eid, int op, int id, const void* event); -int APS5_VABI sceNetEpollWait(int eid, void* events, int maxevents, int timeout); -std::int64_t APS5_VABI sceNetRecv(int s, void* buf, std::size_t len, int flags); -int APS5_VABI sceNetResolverAbort(int rid, int flags); -int APS5_VABI sceNetResolverGetError(int rid, int* status); -std::int64_t APS5_VABI sceNetSend(int s, const void* buf, std::size_t len, int flags); -int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval, std::uint32_t optlen); -int APS5_VABI sceSpNetResolverStartNtoa(int rid, const char* hostname, void* addr, int timeout, int retry, int flags); -int APS5_VABI sceSpPthreadAttrSetinheritsched(PthreadAttr* attr, int inheritSched); -int APS5_VABI sceSpPthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param); -int APS5_VABI sceSpPthreadMutexattrSetprotocol(PthreadMutexattr* attr, int protocol); -int APS5_VABI sceSpPthreadSetprio(Pthread thread, int prio); -int APS5_VABI sceNetResolverStartNtoa(int rid, const char* hostname, void* addr, int timeout, int retry, int flags); -int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr* attr, int inheritSched); -int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param); -int APS5_VABI scePthreadMutexattrSetprotocol(PthreadMutexattr* attr, int protocol); -int APS5_VABI scePthreadSetprio(Pthread thread, int prio); -Pthread APS5_VABI scePthreadSelf(); -} - namespace { void Require(bool value) { if (!value) std::abort(); } From 2aa0a94cce9c4a94f908ca97d1591e61ea2f4f4d Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 16:44:18 +0000 Subject: [PATCH 280/428] feat(libSceBgft): declare sceBgftServiceIntDownloadGetPatchProgress and GetTaskInfo with known signatures Both throw through NotImplemented_nid_no_patch as before; only the parameter lists change, from the OpenOrbis Bgft headers. --- core/libs/prx/libSceBgft/Export.cpp | 4 ++-- core/libs/prx/libSceBgft/include/Api.hpp | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceBgft/Export.cpp b/core/libs/prx/libSceBgft/Export.cpp index 2d5833b1d3..c94cfeb6b8 100644 --- a/core/libs/prx/libSceBgft/Export.cpp +++ b/core/libs/prx/libSceBgft/Export.cpp @@ -85,7 +85,7 @@ int APS5_VABI sceBgftServiceIntDownloadGetGameAndGameAcProgress(const char* cont NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBgftServiceIntDownloadGetPatchProgress() { +int APS5_VABI sceBgftServiceIntDownloadGetPatchProgress(const char* contentId, BgftProgress* progress) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -97,7 +97,7 @@ int APS5_VABI sceBgftServiceIntDownloadGetProgress(int32_t taskId, BgftProgress* NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBgftServiceIntDownloadGetTaskInfo() { +int APS5_VABI sceBgftServiceIntDownloadGetTaskInfo(int32_t taskId, BgftTaskInfo* taskInfo) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceBgft/include/Api.hpp b/core/libs/prx/libSceBgft/include/Api.hpp index 484cf37682..9e39227e29 100644 --- a/core/libs/prx/libSceBgft/include/Api.hpp +++ b/core/libs/prx/libSceBgft/include/Api.hpp @@ -8,6 +8,7 @@ struct BgftInitParam { }; struct BgftProgress; +struct BgftTaskInfo; struct BgftStorageTaskParam { std::uint32_t value0; @@ -47,10 +48,10 @@ int APS5_VABI sceBgftServiceDownloadStopTask(int32_t taskId); int APS5_VABI sceBgftServiceIntDownloadFindActiveGameAndGameAcTask(const char* contentId, int32_t* taskId); int APS5_VABI sceBgftServiceIntDownloadFindActiveTask(const char* contentId, int32_t subType, int32_t* taskId); int APS5_VABI sceBgftServiceIntDownloadGetGameAndGameAcProgress(const char* contentId, BgftProgress* progress); -int APS5_VABI sceBgftServiceIntDownloadGetPatchProgress(); +int APS5_VABI sceBgftServiceIntDownloadGetPatchProgress(const char* contentId, BgftProgress* progress); int APS5_VABI sceBgftServiceIntDownloadGetPlayGoProgress(const char* contentId, BgftProgress* progress); int APS5_VABI sceBgftServiceIntDownloadGetProgress(int32_t taskId, BgftProgress* progress); -int APS5_VABI sceBgftServiceIntDownloadGetTaskInfo(); +int APS5_VABI sceBgftServiceIntDownloadGetTaskInfo(int32_t taskId, BgftTaskInfo* taskInfo); int APS5_VABI sceBgftServiceIntDownloadPauseTask(int32_t taskId); int APS5_VABI sceBgftServiceIntDownloadRegisterTaskByStorageEx(const BgftStorageTaskParam* params, int32_t* taskId); int APS5_VABI sceBgftServiceIntDownloadResumeTask(int32_t taskId); From 11b61f90f7d14418336e013a16c2f72044a90382 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 16:44:18 +0000 Subject: [PATCH 281/428] docs(libSceBgft): record the unmeasured signatures of GetPatchProgress and GetTaskInfo --- docs/dev/debt/libSceBgft.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceBgft.md b/docs/dev/debt/libSceBgft.md index 2240209a4d..95b8d7ac06 100644 --- a/docs/dev/debt/libSceBgft.md +++ b/docs/dev/debt/libSceBgft.md @@ -4,7 +4,7 @@ - [sceBgftServiceDownloadGetProgress](../../../core/libs/prx/libSceBgft/Export.cpp), `sceBgftServiceIntDownloadGetProgress`, `sceBgftServiceIntDownloadGetGameAndGameAcProgress` and `sceBgftServiceIntDownloadGetPlayGoProgress`: signatures and the shared 88-byte output are confirmed. The library forwards the output through IPC without accessing individual fields; their types remain unresolved. - [sceBgftServiceIntDownloadRegisterTaskByStorageEx](../../../core/libs/prx/libSceBgft/Export.cpp): confirmed parameter pointer and 32-bit task ID output. The input contains 32-bit scalars at offsets 0, 4, 56 and 104, strings at offsets 8, 16, 32, 40, 48, 64, 72, 80 and 88, and a 64-bit scalar at offset 96. The typed layout preserves the unused eight bytes at offset 24; scalar meanings and several string roles remain unresolved. -- [sceBgftServiceIntDownloadGetPatchProgress](../../../core/libs/prx/libSceBgft/Export.cpp) and `sceBgftServiceIntDownloadGetTaskInfo`: neither export nor a caller was found in the supplied decompiled library corpus. Their signatures remain unresolved. +- [sceBgftServiceIntDownloadGetPatchProgress](../../../core/libs/prx/libSceBgft/Export.cpp) and `sceBgftServiceIntDownloadGetTaskInfo`: both throw through `NotImplemented_nid_no_patch`; neither export nor a caller was found in the supplied decompiled library corpus. The `(const char* contentId, BgftProgress*)` and `(int32_t taskId, BgftTaskInfo*)` signatures follow the OpenOrbis headers, whose other Bgft prototypes match the confirmed ones here; they are not measured on a console. The task info layout is unknown. - [sceBgftServiceIntDebugPlayGoClearSetFreeZone](../../../core/libs/prx/libSceBgft/Export.cpp) (libSceBgft) - It is in the intersection of the export table of the console's libSceBgft (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceBgft.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. It is declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceBgftServiceIntDebugPlayGoIsSetFreeZone](../../../core/libs/prx/libSceBgft/Export.cpp) (libSceBgft) - It is in the intersection of the export table of the console's libSceBgft (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceBgft.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. It is declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. From a0a1677b1426dc8b409d22a07ca3d20ae165b51d Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:36:52 +0000 Subject: [PATCH 282/428] feat(libSceNpSignaling): implement sceNpSignalingDeactivateConnection --- core/libs/prx/libSceNpSignaling/Export.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNpSignaling/Export.cpp b/core/libs/prx/libSceNpSignaling/Export.cpp index 1e4d40dcb1..00f307beb5 100644 --- a/core/libs/prx/libSceNpSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSignaling/Export.cpp @@ -162,9 +162,12 @@ int APS5_VABI sceNpSignalingTerminateConnection(int ctxId, int connId) { return SCE_NP_SIGNALING_ERROR_CONN_NOT_FOUND; } -int APS5_VABI sceNpSignalingDeactivateConnection(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpSignalingDeactivateConnection(int ctxId, int connId) { + (void)connId; + std::lock_guard lock(signalingMutex); + if (!initialized) return SCE_NP_SIGNALING_ERROR_NOT_INITIALIZED; + if (!FindContext(ctxId)) return SCE_NP_SIGNALING_ERROR_CTX_NOT_FOUND; + return SCE_NP_SIGNALING_ERROR_CONN_NOT_FOUND; } } From 05643a679fc9fa0236a6d50a96b33744855513b8 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:37:37 +0000 Subject: [PATCH 283/428] test(libSceNpSignaling): cover sceNpSignalingDeactivateConnection --- core/libs/prx/libSceNpSignaling/include/Api.hpp | 2 +- core/libs/tests/GuestNpPs4Imports.cpp | 16 ---------------- core/libs/tests/GuestNpSignalingOffline.cpp | 3 +++ 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceNpSignaling/include/Api.hpp b/core/libs/prx/libSceNpSignaling/include/Api.hpp index 1488e7e3da..d11753e6d8 100644 --- a/core/libs/prx/libSceNpSignaling/include/Api.hpp +++ b/core/libs/prx/libSceNpSignaling/include/Api.hpp @@ -12,6 +12,6 @@ int APS5_VABI sceNpSignalingInitialize(std::int64_t memorySize, int threadPriori int APS5_VABI sceNpSignalingSetContextOption(int ctxId, int optionId, int optionValue); int APS5_VABI sceNpSignalingTerminate(); int APS5_VABI sceNpSignalingTerminateConnection(int ctxId, int connId); -int APS5_VABI sceNpSignalingDeactivateConnection(void); +int APS5_VABI sceNpSignalingDeactivateConnection(int ctxId, int connId); } diff --git a/core/libs/tests/GuestNpPs4Imports.cpp b/core/libs/tests/GuestNpPs4Imports.cpp index b863dd65b5..4ffc79c2fe 100644 --- a/core/libs/tests/GuestNpPs4Imports.cpp +++ b/core/libs/tests/GuestNpPs4Imports.cpp @@ -31,17 +31,6 @@ void Require(bool condition, const char* message) { } } -template -void RequireNotImplemented(TCall call, const std::string& name) { - try { - call(); - } catch (const std::runtime_error& error) { - Require(std::string(error.what()) == name + " not implemented", "a declared function threw another error"); - return; - } - Require(false, "a declared function returned"); -} - void WriteBigEndian(std::vector& bytes, std::size_t at, std::uint64_t value, std::size_t width) { for (std::size_t index = 0; index < width; ++index) bytes[at + index] = static_cast(value >> (8u * (width - 1u - index))); } @@ -129,15 +118,10 @@ void CheckMatching2() { Require(sceNpMatching2Terminate() == 0, "NpMatching2 did not terminate after the SSL form"); } -void CheckSignaling() { - RequireNotImplemented([] { sceNpSignalingDeactivateConnection(); }, "sceNpSignalingDeactivateConnection"); -} - } int main() { CheckTrophyInfo(); CheckMatching2(); - CheckSignaling(); return 0; } diff --git a/core/libs/tests/GuestNpSignalingOffline.cpp b/core/libs/tests/GuestNpSignalingOffline.cpp index 4bc7234683..f457b0aeae 100644 --- a/core/libs/tests/GuestNpSignalingOffline.cpp +++ b/core/libs/tests/GuestNpSignalingOffline.cpp @@ -61,6 +61,7 @@ int main() { Require(sceNpSignalingSetContextOption(1, 1, 1) == NotInitialized, "sceNpSignalingSetContextOption", "a call before initialization did not return NOT_INITIALIZED"); Require(sceNpSignalingActivateConnection(1, nullptr, nullptr) == NotInitialized, "sceNpSignalingActivateConnection", "a call before initialization did not return NOT_INITIALIZED first"); Require(sceNpSignalingTerminateConnection(1, 1) == NotInitialized, "sceNpSignalingTerminateConnection", "a call before initialization did not return NOT_INITIALIZED"); + Require(sceNpSignalingDeactivateConnection(1, 1) == NotInitialized, "sceNpSignalingDeactivateConnection", "a call before initialization did not return NOT_INITIALIZED"); Require(sceNpSignalingGetConnectionInfo(1, 1, 1, nullptr) == NotInitialized, "sceNpSignalingGetConnectionInfo", "a call before initialization did not return NOT_INITIALIZED first"); Require(sceNpSignalingTerminate() == NotInitialized, "sceNpSignalingTerminate", "terminating before initialization did not return NOT_INITIALIZED"); @@ -104,6 +105,8 @@ int main() { Require(sceNpSignalingTerminateConnection(99, 1) == CtxNotFound, "sceNpSignalingTerminateConnection", "an unknown context was taken"); Require(sceNpSignalingTerminateConnection(context, 1) == ConnNotFound, "sceNpSignalingTerminateConnection", "a connection that was never created was found"); + Require(sceNpSignalingDeactivateConnection(99, 1) == CtxNotFound, "sceNpSignalingDeactivateConnection", "an unknown context was taken"); + Require(sceNpSignalingDeactivateConnection(context, 1) == ConnNotFound, "sceNpSignalingDeactivateConnection", "a connection that was never created was found"); Require(sceNpSignalingGetConnectionInfo(99, 1, 1, nullptr) == InvalidArgument, "sceNpSignalingGetConnectionInfo", "a null output was not refused before the context"); Require(sceNpSignalingGetConnectionInfo(99, 1, 1, info) == CtxNotFound, "sceNpSignalingGetConnectionInfo", "an unknown context was taken"); Require(sceNpSignalingGetConnectionInfo(context, 1, 1, info) == ConnNotFound, "sceNpSignalingGetConnectionInfo", "a connection that was never created was found"); From 064c370e03c67294913994b6a532510bffaa0f1a Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:17:30 +0000 Subject: [PATCH 284/428] docs(libSceNpSignaling): record sceNpSignalingDeactivateConnection --- docs/dev/debt/libSceNpSignaling.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/dev/debt/libSceNpSignaling.md b/docs/dev/debt/libSceNpSignaling.md index c0dba40607..1adbc6177b 100644 --- a/docs/dev/debt/libSceNpSignaling.md +++ b/docs/dev/debt/libSceNpSignaling.md @@ -1,9 +1,7 @@ # libSceNpSignaling technical debt -### Unknown function info - -- [sceNpSignalingDeactivateConnection](../../../core/libs/prx/libSceNpSignaling/Export.cpp) (libSceNpSignaling) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws - ### Functional - [libSceNpSignaling](../../../core/libs/prx/libSceNpSignaling/Export.cpp) (libSceNpSignaling) - offline only, not measured on a PS5: the checks, their order, context ids 1 to 8 and the NpId rule (`term` 0, 3 to 16 characters, the first an ASCII letter or digit, the rest ASCII letters, digits, `_` or `-`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_signaling/np_signaling.cpp#L118-L170) and its [state helpers](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_signaling/np_signaling_state.cpp#L120-L170). `sceNpSignalingInitialize` reserves no heap and starts no thread, so its arguments are not checked, and callbacks are never called. `sceNpSignalingActivateConnection` returns `SCE_NP_ERROR_SIGNED_OUT` after shadPS4's checks up to `OWN_NP_ID`, without its rate limit, and creates no connection, so the connection calls answer `CONN_NOT_FOUND` + +- [sceNpSignalingDeactivateConnection](../../../core/libs/prx/libSceNpSignaling/Export.cpp) (libSceNpSignaling) - not measured on a PS5: it has the same checks as `sceNpSignalingTerminateConnection` (not initialized, unknown context, then `CONN_NOT_FOUND`), after shadPS4's [call](https://github.com/shadps4-emu/shadPS4/blob/12a4564543ccec527f9723676d2180580d1d7a94/src/core/libraries/np/np_signaling/np_signaling.cpp#L378-L400). No connection is ever created, so none can be deactivated. From dc31bdea09e5ffe8f01ffc15b7a76abdf314e773 Mon Sep 17 00:00:00 2001 From: dantdmdarealguy Date: Thu, 22 Oct 2026 00:36:26 -0500 Subject: [PATCH 285/428] test(libkernel): measure the remapped direct memory charge in the test process CheckRemappedDirectMemoryIsChargedOnce compared the system-wide commit total (K32GetPerformanceInfo) before and after mapping, unmapping and remapping 256 MiB of direct memory, and required the growth to stay under 384 MiB in one of three attempts. Every other process on the host changes that total, so guest_memory failed at GuestMemory.cpp:2164 whenever a parallel test or a compiler committed and freed more than 128 MiB during each attempt. Measure the private and shared commit of the test process instead (PrivateUsage and SharedCommitUsage of PROCESS_MEMORY_COUNTERS_EX2), which covers both the private resident run and the direct memory section. The remap grows it by 265 MiB on every run, so the attempts are dropped. --- core/libs/tests/GuestMemory.cpp | 46 ++++++++++++++++----------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 7f78313a05..f4f590768c 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -2143,30 +2143,30 @@ static void CheckRemappedDirectMemoryIsChargedOnce() { #ifdef _WIN32 constexpr std::size_t bytes = 0x10000000; const auto committed = [] { - PERFORMANCE_INFORMATION info{}; - info.cb = sizeof(info); - Require(K32GetPerformanceInfo(&info, sizeof(info)) != 0); - return static_cast(info.CommitTotal) * static_cast(info.PageSize); + struct { + PROCESS_MEMORY_COUNTERS_EX counters; + SIZE_T privateWorkingSetSize; + ULONG64 sharedCommitUsage; + } info{}; + info.counters.cb = sizeof(info); + Require(K32GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast(&info), sizeof(info)) != 0); + return static_cast(info.counters.PrivateUsage) + static_cast(info.sharedCommitUsage); }; - bool once = false; - for (int attempt = 0; attempt < 3 && !once; ++attempt) { - const auto before = committed(); - std::int64_t phys = 0; - Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, 0, 0, &phys) == 0); - void* mapped = nullptr; - Require(sceKernelMapDirectMemory(&mapped, bytes, 3, 0, phys, 0) == 0); - RequireResidentMapping(mapped); - std::memset(mapped, 0xa5, bytes); - Require(sceKernelMunmap(reinterpret_cast(mapped), bytes) == 0); - void* again = nullptr; - Require(sceKernelMapDirectMemory(&again, bytes, 3, 0, phys, 0) == 0); - const auto* data = static_cast(again); - Require(data[0] == 0xa5 && data[bytes / 2] == 0xa5 && data[bytes - 1] == 0xa5); - once = committed() - before < static_cast(bytes + bytes / 2); - Require(sceKernelMunmap(reinterpret_cast(again), bytes) == 0); - Require(sceKernelReleaseDirectMemory(phys, bytes) == 0); - } - Require(once); + const auto before = committed(); + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, bytes, 3, 0, phys, 0) == 0); + RequireResidentMapping(mapped); + std::memset(mapped, 0xa5, bytes); + Require(sceKernelMunmap(reinterpret_cast(mapped), bytes) == 0); + void* again = nullptr; + Require(sceKernelMapDirectMemory(&again, bytes, 3, 0, phys, 0) == 0); + const auto* data = static_cast(again); + Require(data[0] == 0xa5 && data[bytes / 2] == 0xa5 && data[bytes - 1] == 0xa5); + Require(committed() - before < static_cast(bytes + bytes / 2)); + Require(sceKernelMunmap(reinterpret_cast(again), bytes) == 0); + Require(sceKernelReleaseDirectMemory(phys, bytes) == 0); #endif } From 10c2a645a3d920913ef9e7ad53504dd495092990 Mon Sep 17 00:00:00 2001 From: Disaster Date: Fri, 23 Oct 2026 00:24:14 +0300 Subject: [PATCH 286/428] feat(libc): implement long-double Dinkumware helpers Implement _LLog, _LDivide and _LRecip for the PS5 libc surface, with guest coverage for base dispatch, division and reciprocal semantics. --- core/libs/prx/libc/include/Api.hpp | 3 +++ core/libs/prx/libc/src/DinkumwareInternals.cpp | 13 +++++++++++++ core/libs/tests/GuestDinkumwareInternals.cpp | 12 ++++++++++++ docs/dev/debt/libc.md | 1 + 4 files changed, 29 insertions(+) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index afdb17d63c..8b08397c5e 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -200,6 +200,9 @@ float APS5_VABI _FSin_nid_postfix(float x, unsigned int quadrant); long double APS5_VABI _LSin_nid_postfix(long double x, unsigned int quadrant); double APS5_VABI _Log_nid_postfix(double x, int base); float APS5_VABI _FLog_nid_postfix(float x, int base); +long double APS5_VABI _LLog_nid_postfix(long double x, int base); +long double APS5_VABI _LDivide_nid_postfix(long double x, long double y); +long double APS5_VABI _LRecip_nid_postfix(long double x); void* APS5_VABI opendir_nid_postfix(const char* path); GuestDirectoryEntry* APS5_VABI readdir_nid_postfix(void* handle); int APS5_VABI readdir_r_nid_postfix(void* handle, GuestDirectoryEntry* entry, GuestDirectoryEntry** result); diff --git a/core/libs/prx/libc/src/DinkumwareInternals.cpp b/core/libs/prx/libc/src/DinkumwareInternals.cpp index d1f8ac33f4..b8f6ce21c5 100644 --- a/core/libs/prx/libc/src/DinkumwareInternals.cpp +++ b/core/libs/prx/libc/src/DinkumwareInternals.cpp @@ -153,4 +153,17 @@ float APS5_VABI _FLog_nid_postfix(float x, int base) { return base == 0 ? logf_nid_postfix(x) : log2f_nid_postfix(x); } +long double APS5_VABI _LLog_nid_postfix(long double x, int base) { + if (base > 0) return log10l_nid_postfix(x); + return base == 0 ? logl_nid_postfix(x) : log2l_nid_postfix(x); +} + +long double APS5_VABI _LDivide_nid_postfix(long double x, long double y) { + return x / y; +} + +long double APS5_VABI _LRecip_nid_postfix(long double x) { + return 1.0L / x; +} + } diff --git a/core/libs/tests/GuestDinkumwareInternals.cpp b/core/libs/tests/GuestDinkumwareInternals.cpp index 6e1ffdf7e4..0e1f538ca8 100644 --- a/core/libs/tests/GuestDinkumwareInternals.cpp +++ b/core/libs/tests/GuestDinkumwareInternals.cpp @@ -166,6 +166,17 @@ void TestDispatch() { Require(_FLog_nid_postfix(8.0f, 0) == std::log(8.0f) && _FLog_nid_postfix(100.0f, 1) == 2.0f && _FLog_nid_postfix(8.0f, -3) == 3.0f, "_FLog"); } +void TestLongDoubleHelpers() { + Require(_LDivide_nid_postfix(3.0L, 2.0L) == 1.5L, "_LDivide quotient"); + Require(std::isinf(_LDivide_nid_postfix(1.0L, 0.0L)), "_LDivide infinity"); + Require(std::isnan(_LDivide_nid_postfix(0.0L, 0.0L)), "_LDivide invalid"); + Require(_LRecip_nid_postfix(4.0L) == 0.25L, "_LRecip quotient"); + Require(std::isinf(_LRecip_nid_postfix(-0.0L)) && std::signbit(_LRecip_nid_postfix(-0.0L)), "_LRecip signed infinity"); + Require(_LLog_nid_postfix(8.0L, 0) == std::log(8.0L), "_LLog natural"); + Require(std::fabs(_LLog_nid_postfix(1000.0L, 1) - 3.0L) <= 1e-17L && std::fabs(_LLog_nid_postfix(1000.0L, 5) - 3.0L) <= 1e-17L, "_LLog base 10"); + Require(_LLog_nid_postfix(8.0L, -1) == 3.0L && _LLog_nid_postfix(8.0L, -3) == 3.0L, "_LLog base 2"); +} + } int main() { @@ -174,4 +185,5 @@ int main() { TestFloatErrors(); TestIntegerErrors(); TestDispatch(); + TestLongDoubleHelpers(); } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 2d4e0fc01d..9c651f03b4 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -98,6 +98,7 @@ - [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `strtod` and `wcstod` (and `atof`, which calls `strtod` here) read a `NAN(...)` sequence as FreeBSD 11's gdtoa [`hexnan`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/hexnan.c) does: hexadecimal digits with an optional `0x`, blanks starting a new 32-bit word, the first 16 digits kept and the payload cut to 52 bits, a zero payload made 1, any other character giving the default NaN but consuming up to the closing parenthesis, and an unterminated sequence keeping its payload while consuming only `nan`. The payload goes under the quiet bit with the sign, as in gdtoa's [`strtod`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/strtod.c#L316-L328). `strtof`, `wcstof`, `strtold` and `wcstold` return the positive default quiet NaN (`0x7fc00000`; exponent `0x7fff`, significand `0xc000000000000000`) whatever the sign and the sequence, and consume a sequence of letters, digits and underscores up to its closing parenthesis. The console's `libSceLibcInternal.sprx`, called from a payload process on a PS5 (firmware 12.70), gives these results for the 64 NaN inputs tried through `strtod` and `strtof`, 50 of them also through `strtold` and `wcstod`, and 18 through `wcstof` and `wcstold`, and, through `strtod`, for `nan(1 2)` and for `nan( ` followed by NUL and `)`; that a title's own `libc.prx` does the same is assumed. In `hexnan`, a blank after a payload digit, while a 32-bit word of the payload is still free, starts a blank skip that has no NUL check, so when only blanks follow up to the terminating NUL the console's `strtod` reads past it: `nan(1 ` followed by NUL and `)` consumes the `)` after the NUL, so the result depends on the bytes after the string, and here that case throws. A blank right after `nan(` is skipped with a NUL check, and the console stopped at the NUL there - [remquol](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) leaves `*quo` unchanged when the result is NaN; the `libc.prx` bundled with PPSA08427 writes a value that depends on earlier x87 state (`remquol(1, 0)` stored 0 on a first call and 4 on later ones) - [_Stod, _Stodx, _Stofx, _Stoldx, _Stof and _Stold](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libSceLibcInternal, implemented in libc) throw for hexadecimal text with a non-zero power-of-ten scale. The console's result there does not follow a simple rule: `0x3` with a scale of -1 gives 0.29999999999999999. A nonzero subnormal result sets neither `errno` nor the error argument, as measured in firmware 13.60's `libSceLibcInternal.sprx` run in a Linux host process, not on a console; there about a fifth of decimal inputs with a subnormal result come out one unit apart in the last place (`_Stodx("2.2250738585072014e-308", 0)` is the largest subnormal), which is not reproduced +- [_LDivide, _LRecip and _LLog](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libc) follow the public Dinkumware CRT contracts: long-double division and reciprocal, and the `_LLog` base flag (`0` natural, positive base 10, negative base 2). The exact console results, signaling-NaN behavior and floating-point exception behavior are not verified. - [Long double math in MathExact.cpp](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) was compared with the `libc.prx` bundled with PPSA08427 only for canonical 80-bit encodings; unnormals, pseudo-denormals and pseudo-infinities passed to arithmetic functions (not the classifiers) follow the host x87 instead of the console - [atanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`s_atanf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_atanf.c), with `atanlo[3]` read at run time as msun's `volatile` does. Tiny arguments return `x` without raising `FE_INEXACT`. It gives the same bits as the `atanf` of the `libc.prx` bundled with PPSA07923, PPSA05916 and PPSA13934 for all 2^32 arguments, run on a Windows host, and none of those sets `errno`. Whether other titles' builds differ is unverified. - [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `rmdir("dir/.")`, `unlink("dir/.")` and the same calls with `..` follow the same lookup rule in FreeBSD but are not changed here: the guest still resolves the dot component away and acts on `dir` or its parent. Both arguments are checked before any host access, so `rename` of a missing source onto `dir/.` answers `EINVAL`, where FreeBSD's [`kern_renameat`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_syscalls.c) looks the source up first and answers `ENOENT`. A backslash counts as a path separator here (the guest path resolver turns it into `/`); FreeBSD treats it as an ordinary name character. None of this was measured on a console. From b3fec9cce0ab9ed285f68430f8efda877f67b13f Mon Sep 17 00:00:00 2001 From: hypermist Date: Sun, 25 Oct 2026 12:42:14 +1300 Subject: [PATCH 287/428] refactor(libSceSpSysCallWrapper): declare sceSpSelect signature in the library header Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceSpSysCallWrapper/include/Api.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceSpSysCallWrapper/include/Api.hpp b/core/libs/prx/libSceSpSysCallWrapper/include/Api.hpp index f53f02a76b..10979d9d31 100644 --- a/core/libs/prx/libSceSpSysCallWrapper/include/Api.hpp +++ b/core/libs/prx/libSceSpSysCallWrapper/include/Api.hpp @@ -53,6 +53,6 @@ int APS5_VABI sceSpPthreadMutexattrSetprioceiling(); int APS5_VABI sceSpPthreadMutexattrSetprotocol(PthreadMutexattr* attr, int protocol); int APS5_VABI sceSpPthreadMutexattrSettype(PthreadMutexattr* attr, int type); int APS5_VABI sceSpPthreadSetprio(Pthread thread, int prio); -int APS5_VABI sceSpSelect(); +int APS5_VABI sceSpSelect(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout); } From 02c9290d77e2cf6322472af20d8746ccbf72cbae Mon Sep 17 00:00:00 2001 From: hypermist Date: Sun, 25 Oct 2026 12:45:44 +1300 Subject: [PATCH 288/428] refactor(libSceFont): use the library headers in the font metrics test Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestFontMetrics.cpp | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/core/libs/tests/GuestFontMetrics.cpp b/core/libs/tests/GuestFontMetrics.cpp index 5f77094ef2..24d5f2b1ca 100644 --- a/core/libs/tests/GuestFontMetrics.cpp +++ b/core/libs/tests/GuestFontMetrics.cpp @@ -1,4 +1,6 @@ #include "prx/libSceFont/include/FontDriver.hpp" +#include "prx/libSceFont/libSceFont.hpp" +#include "prx/libSceFontFt/libSceFontFt.hpp" #include "tests/FontFixture.hpp" #include #include @@ -8,21 +10,6 @@ #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 sceFontSupportExternalFonts(FontLibrary, std::uint32_t, std::uint32_t); -int APS5_VABI sceFontOpenFontMemory(FontLibrary, const void*, std::uint32_t, const FontOpenDetail*, FontHandle*); -int APS5_VABI sceFontCloseFont(FontHandle); -int APS5_VABI sceFontSetScalePixel(FontHandle, float, float); -int APS5_VABI sceFontSetEffectWeight(FontHandle, float, float, std::uint32_t); -int APS5_VABI sceFontSetEffectSlant(FontHandle, float); -int APS5_VABI sceFontGetFontMetrics(FontHandle, std::uint32_t, FontMetrics*); -const void* APS5_VABI sceFontSelectLibraryFt(int); -} - static void Check(bool value, int line) { if (!value) { std::fprintf(stderr, "Font metrics check failed at line %d\n", line); From 6457edc2fb78367687269e5db05d44ef340e31c1 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Thu, 22 Oct 2026 02:51:19 +0200 Subject: [PATCH 289/428] fix(libkernel): fail fcntl F_DUPFD with EINVAL when the minimum descriptor is beyond the open file limit on Windows fcntl(fd, F_DUPFD, min) with min at or above RLIMIT_NOFILE duplicated the descriptor repeatedly until the CRT table was full and then failed with EMFILE, after opening thousands of descriptors. FreeBSD checks the minimum first: kern_dup returns EINVAL for FDDUP_FCNTL when new < 0 or new >= maxfd (sys/kern/kern_descrip.c:836-840). Linux already answers EINVAL through the host fcntl. Check the minimum against the guest RLIMIT_NOFILE before the Windows loop. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/File/src/Stdio.cpp | 2 ++ core/libs/tests/GuestDup.cpp | 3 +++ 2 files changed, 5 insertions(+) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 338dc6c3a8..d1adbbc74e 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -627,6 +627,8 @@ static bool HasNonDirectoryPrefix(const std::filesystem::path& native) { int File::DuplicateFrom(int fd, int start) { #ifdef _WIN32 + GuestResourceLimit openFileLimit{}; + if (getrlimit_nid_postfix(8, &openFileLimit) == 0 && start >= openFileLimit.rlim_cur) return PosixFailure(GUEST_EINVAL); std::vector below; int duplicate; while ((duplicate = dup_nid_postfix(fd)) >= 0 && duplicate < start) below.push_back(duplicate); diff --git a/core/libs/tests/GuestDup.cpp b/core/libs/tests/GuestDup.cpp index cb2ce4f36e..03db1dcba6 100644 --- a/core/libs/tests/GuestDup.cpp +++ b/core/libs/tests/GuestDup.cpp @@ -1,6 +1,7 @@ #include "prx/libkernel/include/Api.hpp" #include "prx/libc/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -187,6 +188,8 @@ static void DuplicatesShareStatusFlags() { Require(pipe_nid_postfix(pipe) == 0); Require(pipe_nid_postfix(other) == 0); Require(fcntl_nid_postfix(pipe[0], SetDescriptorFlags, 1) == 0); + *__error_nid_postfix() = 0; + Require(fcntl_nid_postfix(pipe[0], 0, INT_MAX) == -1 && *__error_nid_postfix() == 22); const int duplicate = dup_nid_postfix(pipe[0]); Require(duplicate >= 0); Require(fcntl_nid_postfix(duplicate, GetDescriptorFlags) == 0); From a63d9619167200b7249280f401ad30c03bb3a4b6 Mon Sep 17 00:00:00 2001 From: harisibrar1 <207127467+harisibrar1@users.noreply.github.com> Date: Sat, 24 Oct 2026 19:25:13 -0400 Subject: [PATCH 290/428] fix(libkernel): clear select bits from nfds to the end of their word as FreeBSD copies them out Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Socket/src/Socket.cpp | 10 ++++++ core/libs/tests/GuestSelect.cpp | 33 +++++++++++++++++-- core/libs/tests/GuestSelectWait.cpp | 7 ++-- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Socket.cpp b/core/libs/prx/libkernel/Socket/src/Socket.cpp index e27ce66b87..7fb42adbcb 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -51,6 +51,14 @@ void ClearDescriptor(void* set, int descriptor) { if (set != nullptr) static_cast(set)[descriptor / 64] &= ~(std::uint64_t{1} << (descriptor % 64)); } +void ClearDescriptorsFrom(int nfds, void* readfds, void* writefds, void* exceptfds) { + if (nfds % 64 == 0) return; + const std::uint64_t kept = (std::uint64_t{1} << (nfds % 64)) - 1; + for (void* set : {readfds, writefds, exceptfds}) { + if (set != nullptr) static_cast(set)[nfds / 64] &= kept; + } +} + struct PollGroup { KernelSocketPoll::Poller poll; std::vector entries; @@ -311,6 +319,7 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* if ((entry.revents & (KernelSocketPoll::Writable | KernelSocketPoll::Error)) == 0) ClearDescriptor(writefds, entry.descriptor); if ((entry.revents & KernelSocketPoll::Urgent) == 0) ClearDescriptor(exceptfds, entry.descriptor); } + ClearDescriptorsFrom(nfds, readfds, writefds, exceptfds); return ready; } if (limit != nullptr && std::chrono::steady_clock::now() >= deadline) { @@ -319,6 +328,7 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* ClearDescriptor(writefds, entry.descriptor); ClearDescriptor(exceptfds, entry.descriptor); } + ClearDescriptorsFrom(nfds, readfds, writefds, exceptfds); return 0; } } diff --git a/core/libs/tests/GuestSelect.cpp b/core/libs/tests/GuestSelect.cpp index 55c7be7b9a..23cb18d6cf 100644 --- a/core/libs/tests/GuestSelect.cpp +++ b/core/libs/tests/GuestSelect.cpp @@ -185,14 +185,43 @@ int main() { Require(read.Has(reader) && write.Has(writer)); } + Require(reader < 63); + + { + GuestDescriptorSet read{}; + GuestDescriptorSet except{}; + read.Add(reader); + read.Add(63); + read.Add(64); + except.Add(63); + GuestTimeval limit{0, 100000}; + Require(select_nid_postfix(reader, read.words.data(), nullptr, except.words.data(), &limit) == 0); + Require(!read.Has(reader) && !read.Has(63) && read.Has(64) && !except.Has(63)); + } + { GuestDescriptorSet read{}; read.Add(reader); - GuestTimeval limit{0, 100000}; - Require(select_nid_postfix(reader, read.words.data(), nullptr, nullptr, &limit) == 0); + const GuestTimeval zero{0, 0}; + Require(select_nid_postfix(0, read.words.data(), nullptr, nullptr, &zero) == 0); Require(read.Has(reader)); } + { + const char byte = 'n'; + Require(write_nid_postfix(writer, &byte, 1) == 1); + GuestDescriptorSet read{}; + read.Add(reader); + read.Add(63); + read.Add(64); + GuestTimeval limit{0, 200000}; + Require(select_nid_postfix(reader + 1, read.words.data(), nullptr, nullptr, &limit) == 1); + Require(read.Has(reader) && !read.Has(63) && read.Has(64)); + char buffer[8]{}; + const auto count = read_nid_postfix(reader, buffer, sizeof(buffer)); + Require(count >= 1 && buffer[count - 1] == byte); + } + { GuestDescriptorSet read{}; read.Add(reader); diff --git a/core/libs/tests/GuestSelectWait.cpp b/core/libs/tests/GuestSelectWait.cpp index 3c9044117d..e5dd072cd4 100644 --- a/core/libs/tests/GuestSelectWait.cpp +++ b/core/libs/tests/GuestSelectWait.cpp @@ -53,14 +53,17 @@ int main() { } std::array read{}; read[0] = 1 | (std::uint64_t{1} << 63); + read[1] = 1; const GuestTimeval limit{0, 500}; const auto start = Clock::now(); Require(select_nid_postfix(1, read.data(), nullptr, nullptr, &limit) == 0, "select with no ready device did not time out"); Require(Clock::now() - start >= std::chrono::microseconds(limit.microseconds), "select with no ready device returned before its deadline"); - Require(read[0] == (std::uint64_t{1} << 63), - "select did not preserve bits outside the descriptor limit"); + Require(read[0] == 0, + "select did not clear the bits up to the end of the last copied word"); + Require(read[1] == 1, + "select did not preserve the words past the descriptor limit"); std::array write{}; write[0] = 2; const GuestTimeval longLimit{1, 0}; From 27ee186e36b177f95a1341e8549598c11219a75f Mon Sep 17 00:00:00 2001 From: hypermist Date: Sun, 25 Oct 2026 12:58:26 +1300 Subject: [PATCH 291/428] refactor(libkernel_sys): include the library header in the constants test Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestKernelSysConstants.cpp | 24 +-------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/core/libs/tests/GuestKernelSysConstants.cpp b/core/libs/tests/GuestKernelSysConstants.cpp index 418f98071c..8565415fc8 100644 --- a/core/libs/tests/GuestKernelSysConstants.cpp +++ b/core/libs/tests/GuestKernelSysConstants.cpp @@ -1,29 +1,7 @@ #include "SceTypes.hpp" +#include "prx/libkernel_sys/libkernel_sys.hpp" #include -extern "C" { -int APS5_VABI pthread_switch_add_np_nid_postfix(void); -int APS5_VABI pthread_switch_delete_np_nid_postfix(void); -int APS5_VABI sceKernelAddResource(void); -int APS5_VABI sceKernelEventLogInit(void); -int APS5_VABI sceKernelIsGenuineKratosCex(void); -int APS5_VABI sceKernelReleaseTraceDirectMemory(void); -int APS5_VABI sceKernelHasNeoMode(void); -int APS5_VABI sceKernelIsAuthenticNeo(void); -int APS5_VABI sceKernelIsGenuineN(void); -int APS5_VABI sceKernelCreateEport(void); -int APS5_VABI sceKernelDeleteEport(void); -int APS5_VABI sceKernelOpenEport(void); -int APS5_VABI sceKernelCloseEport(void); -int APS5_VABI sceKernelTriggerEport(void); -int APS5_VABI sceKernelQueryTraceMemory(void); -int APS5_VABI sceKernelIccGetCPMode(void); -int APS5_VABI sceKernelIccSetCPMode(void); -int APS5_VABI sceKernelClearGameDirectMemory(void); -int APS5_VABI sceKernelMapTraceMemory(void); -int APS5_VABI sceKernelTraceMemoryTypeProtect(void); -} - namespace { constexpr int GUEST_ENOTSUP = 45; From ad047899bbd14b0b62819c45a07cc6a04a2924ed Mon Sep 17 00:00:00 2001 From: thibaut Date: Sun, 25 Oct 2026 02:02:41 +0200 Subject: [PATCH 292/428] perf(agc): grow the pipeline store from 1024 up to 4096 entries The graphics pipeline store kept a fixed 1024 entries and the pipeline journal replayed at most 1024 records, and a replayed pipeline that found the store full was dropped. A title whose working set is larger rebuilt the rest at the draw on every run, and evicted pipelines it kept using were rebuilt again and again. The bound now starts at 1024 and doubles up to 4096: a journal replay that does not fit grows it instead of dropping the pipeline, and so does eviction churn (a ghost list of evicted keys, checked every bound/32 evictions: more than half of them came back). It never shrinks during a run; APS5_PIPELINE_STORE_ENTRIES still fixes it. The journal loads its last 4096 records by default (APS5_PIPELINE_JOURNAL_ENTRIES). Resident pipelines are bounded by count, not bytes. agc_driver_recorder_tests --benchmark-pipeline-store-memory [count] (opt-in, not registered) builds count graphics pipelines with one fragment module each through CachedPipeline and prints process private bytes and VK_EXT_memory_budget heap usage before and after: with the triangle shaders about 59 KiB host and no device memory per pipeline on an RTX 4090, 100 KiB host and 8 KiB device-local on a UHD 770. Tests: agc_pipeline_store_growth (fails on main), agc_pipeline_store_fixed_bound, agc_pipeline_store_replay_growth (replays on one thread: the mock Vulkan is not thread-safe). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017SNUYRd7sRKZrMYpquAsnU --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 9 +- .../Graphics/include/Pipeline.hpp | 8 +- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 72 ++++++++++- .../Graphics/src/PipelineJournal.cpp | 4 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 121 ++++++++++++++++++ .../prx/libSceAgcDriver/tests/Recorder.cpp | 112 ++++++++++++++++ docs/dev/ARCHITECTURE.md | 2 +- docs/dev/ENVIRONMENT.md | 4 +- docs/dev/debt/libSceAgcDriver.md | 2 +- docs/user/USAGE.md | 2 +- 10 files changed, 318 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 613780e647..8cfa668c7a 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2721,6 +2721,11 @@ if(BUILD_TESTING) set_tests_properties(agc_pipeline_journal_long_paths PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_pipeline_library_clear_waits_for_build COMMAND agc_driver_graphics_tests --pipeline-library-clear-waits-for-build) add_test(NAME agc_pipeline_store_lazy_buffer_pool COMMAND agc_driver_graphics_tests --pipeline-store-lazy-buffer-pool) + add_test(NAME agc_pipeline_store_growth COMMAND agc_driver_graphics_tests --pipeline-store-growth) + add_test(NAME agc_pipeline_store_fixed_bound COMMAND agc_driver_graphics_tests --pipeline-store-growth) + add_test(NAME agc_pipeline_store_replay_growth COMMAND agc_driver_graphics_tests --pipeline-store-replay-growth) + set_tests_properties(agc_pipeline_store_growth agc_pipeline_store_fixed_bound agc_pipeline_store_replay_growth PROPERTIES TIMEOUT 60 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_pipeline_store_fixed_bound PROPERTIES ENVIRONMENT "APS5_PIPELINE_STORE_ENTRIES=1024") set_tests_properties(agc_pipeline_journal PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(agc_pipeline_library_clear_waits_for_build PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(agc_pipeline_store_lazy_buffer_pool PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") @@ -2728,7 +2733,7 @@ if(BUILD_TESTING) add_test(NAME agc_pipeline_journal_extended_paths COMMAND agc_driver_graphics_tests --pipeline-journal-extended-paths) set_tests_properties(agc_pipeline_journal_extended_paths PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(agc_pipeline_journal_long_paths agc_pipeline_journal_extended_paths PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") - set_tests_properties(agc_pipeline_journal agc_pipeline_library_clear_waits_for_build agc_pipeline_store_lazy_buffer_pool PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_pipeline_journal agc_pipeline_library_clear_waits_for_build agc_pipeline_store_lazy_buffer_pool agc_pipeline_store_growth agc_pipeline_store_fixed_bound agc_pipeline_store_replay_growth PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() set_tests_properties(agc_driver_sparse_buffer_holes PROPERTIES TIMEOUT 30) if(WIN32) @@ -3956,6 +3961,6 @@ endif() if(BUILD_TESTING) get_directory_property(agcRegisteredTests TESTS) - list(FILTER agcRegisteredTests EXCLUDE REGEX "^(agc_pipeline_journal(_long_paths|_extended_paths)?|agc_driver_pipeline_journal)$") + list(FILTER agcRegisteredTests EXCLUDE REGEX "^(agc_pipeline_journal(_long_paths|_extended_paths)?|agc_driver_pipeline_journal|agc_pipeline_store_replay_growth)$") set_property(TEST ${agcRegisteredTests} APPEND PROPERTY ENVIRONMENT "APS5_NO_PIPELINE_JOURNAL=1") endif() diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index abfb3226a6..b9cea62862 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -109,8 +109,10 @@ private: // stages and every pipeline state field. A configuration whose stage has no module gets a private // pipeline. A miss builds the pipeline outside the store's mutex (a concurrent build of the same // key keeps the first inserted) and records its recipe in the pipeline journal. Entries are -// LRU-bounded (APS5_PIPELINE_STORE_ENTRIES, 1024) and only evicted once no recorded draw holds -// them. Debug aid: APS5_NO_PIPELINE_CACHE=1 builds one per draw as before. +// LRU-bounded and only evicted once no recorded draw holds them. The bound starts at 1024 and +// doubles up to 4096 for a journal replay that does not fit and while pipelines it evicted keep +// coming back; it never shrinks during a run. APS5_PIPELINE_STORE_ENTRIES fixes it. Debug aid: +// APS5_NO_PIPELINE_CACHE=1 builds one per draw as before. std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); // `created` reports whether this call built the pipeline (false: served from the store or private). std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, bool* created = nullptr); @@ -124,6 +126,8 @@ struct PipelineStoreCounters { std::uint64_t bound = 0; }; PipelineStoreCounters PipelineStoreTotals(); +// Whether the store may still raise its bound (not fixed by APS5_PIPELINE_STORE_ENTRIES, below 4096). +bool PipelineStoreCanGrow(); // The pipeline store's key of a configuration (empty when the configuration gets a private pipeline). void BuildPipelineStoreKey(std::vector& key, const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout); struct DescribedSetLayouts { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index fcad4ff7bf..db5c53e611 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -1126,6 +1126,14 @@ struct PipelineStore { std::uint64_t evicted = 0; PipelineStoreCounters totals; std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now(); + std::size_t capacity = 0; + bool fixedCapacity = false; + std::list evictedOrder; + std::unordered_map::iterator> evictedKeys; + std::size_t evictionsSinceCheck = 0; + std::size_t returnsSinceCheck = 0; + static constexpr std::size_t BaseEntries = 1024; + static constexpr std::size_t MaximumEntries = 4096; }; // Never destroyed: the pipelines belong to a device that may already be gone when statics die, and @@ -1154,9 +1162,41 @@ std::list::iterator abandon(PipelineStore& store, std::lis return store.entries.erase(it); } -std::size_t storeBound() { - static const auto bound = static_cast(std::max(1u, PipelineSwitch("APS5_PIPELINE_STORE_ENTRIES", 1024u))); - return bound; +std::size_t storeBound(PipelineStore& store) { + if (store.capacity == 0) { + const auto set = PipelineSwitch("APS5_PIPELINE_STORE_ENTRIES", 0u); + store.fixedCapacity = set != 0; + store.capacity = store.fixedCapacity ? static_cast(set) : PipelineStore::BaseEntries; + } + return store.capacity; +} + +void noteStoreEviction(PipelineStore& store, std::uint64_t hash) { + if (store.fixedCapacity) return; + if (const auto found = store.evictedKeys.find(hash); found != store.evictedKeys.end()) store.evictedOrder.erase(found->second); + store.evictedOrder.push_front(hash); + store.evictedKeys[hash] = store.evictedOrder.begin(); + while (store.evictedOrder.size() > store.capacity) { + store.evictedKeys.erase(store.evictedOrder.back()); + store.evictedOrder.pop_back(); + } + if (++store.evictionsSinceCheck < store.capacity / 32u) return; + const bool churning = store.returnsSinceCheck * 2u > store.evictionsSinceCheck; + store.evictionsSinceCheck = 0; + store.returnsSinceCheck = 0; + if (!churning || store.capacity >= PipelineStore::MaximumEntries) return; + const auto previous = store.capacity; + store.capacity = std::min(store.capacity * 2u, PipelineStore::MaximumEntries); + std::fprintf(stderr, "[pipecache] bound %zu -> %zu pipelines: pipelines it evicted keep coming back\n", previous, store.capacity); +} + +void noteStoreReturn(PipelineStore& store, std::uint64_t hash) { + if (store.fixedCapacity) return; + const auto found = store.evictedKeys.find(hash); + if (found == store.evictedKeys.end()) return; + store.evictedOrder.erase(found->second); + store.evictedKeys.erase(found); + ++store.returnsSinceCheck; } void reportPipelines(PipelineStore& store) { @@ -1228,12 +1268,12 @@ std::shared_ptr CachedPipeline(const Context& context, const State& st } ++store.misses; ++store.totals.misses; + noteStoreReturn(store, hash); } // Built outside the mutex: a draw that finds its pipeline resident is not held behind another // thread's build (the journal replay builds here too), and a device's driver compiles in // parallel when several threads build at once. auto pipeline = std::make_shared(context, state, vertexInput, resources, shaders, attachmentLayout); - const auto bound = storeBound(); const bool replaying = PipelineJournalReplaying(); { std::lock_guard lock(store.mutex); @@ -1251,7 +1291,10 @@ std::shared_ptr CachedPipeline(const Context& context, const State& st } // A replayed pipeline enters at the least recently used end and never displaces a draw's: // the title's own pipelines evict it first, and a full store takes none. - if (replaying && store.entries.size() >= bound) return pipeline; + if (replaying && store.entries.size() >= storeBound(store)) { + if (store.fixedCapacity || store.capacity >= PipelineStore::MaximumEntries) return pipeline; + store.capacity = std::min(store.capacity * 2u, PipelineStore::MaximumEntries); + } if (replaying) { store.entries.push_front({context.device, context.bufferPool, context.bufferPool != nullptr, hash, std::vector(key), pipeline}); store.index[hash] = store.entries.begin(); @@ -1259,12 +1302,14 @@ std::shared_ptr CachedPipeline(const Context& context, const State& st store.entries.push_back({context.device, context.bufferPool, context.bufferPool != nullptr, hash, std::vector(key), pipeline}); store.index[hash] = std::prev(store.entries.end()); } - while (store.entries.size() > bound) { + while (store.entries.size() > storeBound(store)) { // Only an entry no recorded draw still holds may go (Kept keeps its shared_ptr until the fence). const auto victim = std::find_if(store.entries.begin(), store.entries.end(), [](const PipelineStore::Entry& entry) { return entry.pipeline.use_count() == 1; }); if (victim == store.entries.end()) break; + const auto evictedHash = victim->hash; store.index.erase(victim->hash); store.entries.erase(victim); + noteStoreEviction(store, evictedHash); ++store.evicted; ++store.totals.evicted; } @@ -1283,10 +1328,16 @@ PipelineStoreCounters PipelineStoreTotals() { std::lock_guard lock(store.mutex); auto totals = store.totals; totals.cached = store.entries.size(); - totals.bound = storeBound(); + totals.bound = storeBound(store); return totals; } +bool PipelineStoreCanGrow() { + auto& store = Pipelines(); + std::lock_guard lock(store.mutex); + return storeBound(store) < PipelineStore::MaximumEntries && !store.fixedCapacity; +} + void ClearCachedPipelines(VkDevice device) { auto& store = Pipelines(); std::lock_guard lock(store.mutex); @@ -1298,6 +1349,13 @@ void ClearCachedPipelines(VkDevice device) { store.index.erase(it->hash); it = store.entries.erase(it); } + if (store.entries.empty() && !store.fixedCapacity) { + store.capacity = 0; + store.evictedOrder.clear(); + store.evictedKeys.clear(); + store.evictionsSinceCheck = 0; + store.returnsSinceCheck = 0; + } ClearPipelineLibraries(device); ReleasePixelFunctionLds(device); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index 7eba2c79ac..67a926f157 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -1349,7 +1349,7 @@ private: std::call_once(loadOnce, [&] { try { auto journal = Journal::Instance().Load(); - const auto limit = configured("APS5_PIPELINE_JOURNAL_ENTRIES", 1024u); + const auto limit = configured("APS5_PIPELINE_JOURNAL_ENTRIES", 4096u); if (journal.entries.size() > limit) journal.entries.erase(journal.entries.begin(), journal.entries.end() - static_cast(limit)); loaded = std::move(journal); } catch (const std::exception& error) { @@ -1435,7 +1435,7 @@ private: return; } const auto store = PipelineStoreTotals(); - if (store.cached >= store.bound) { + if (store.cached >= store.bound && !PipelineStoreCanGrow()) { counters.skipped.fetch_add(1, std::memory_order_relaxed); return; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 7afcaac27e..e9ecf3597f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -11379,6 +11379,117 @@ void pipelineStoreLazyBufferPoolTests() { Require(mock.live == 0, "lazy buffer pool pipeline tests leaked Vulkan objects"); } +// The pipeline store's bound without a GPU: 1536 configurations cycled through the default 1024 +// entries keep coming back after their eviction, so the bound doubles and a later pass finds every +// pipeline resident; a bound set with APS5_PIPELINE_STORE_ENTRIES never moves. +void pipelineStoreGrowthTests(bool fixed) { + using namespace AgcDriver::Graphics; + mock = MockVulkan{}; + auto context = mockContext(); + context.device = makeHandle(); + context.graphicsPipelineLibrary = true; + context.pipelineLinkTimeOptimization = false; + context.limits.framebufferNoAttachmentsSampleCounts = VK_SAMPLE_COUNT_1_BIT; + ShaderRecompiler::RecompileResult vertex, fragment; + vertex.spirv = makeModule({}); + fragment.spirv = makeModule({.fragment = true}); + vertex.variantId = 301; + fragment.variantId = 302; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}}; + std::array mainKey{41}, fragmentKey{42}; + const PipelineSetLayouts resources{makeHandle(), makeHandle(), true, mainKey, fragmentKey}; + constexpr std::uint32_t configurations = 1536; + const auto pass = [&](std::uint32_t count) { + std::uint32_t built = 0; + for (std::uint32_t configuration = 0; configuration < count; ++configuration) { + VertexInputLayout input; + input.bindings.push_back({0, 4u * (configuration + 1u), VK_VERTEX_INPUT_RATE_VERTEX}); + bool created = false; + static_cast(CachedPipeline(context, State{}, input, resources, shaders, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, &created)); + built += created ? 1u : 0u; + } + return built; + }; + std::uint32_t lastBuilt = 0; + for (int round = 0; round < 6; ++round) lastBuilt = pass(configurations); + const auto grown = PipelineStoreTotals(); + if (fixed) { + Require(grown.bound == 1024, "a bound set with APS5_PIPELINE_STORE_ENTRIES changed"); + Require(lastBuilt == configurations, "a fixed 1024-entry bound kept pipelines of a 1536-configuration cycle"); + } else { + Require(grown.bound == 2048, "the bound did not grow to 2048 while evicted pipelines kept coming back"); + Require(lastBuilt == 0, "a pass over 1536 configurations still rebuilt pipelines after the bound grew"); + } + ClearCachedPipelines(context.device); + Require(mock.live == 0, "pipeline store growth tests leaked Vulkan objects"); +} + +// A journal longer than the store's starting bound: 1032 recorded pipelines replayed into an empty +// store all stay resident (the replay grows the bound instead of dropping what does not fit). +void pipelineStoreReplayGrowthTests() { + using namespace AgcDriver::Graphics; + const auto root = std::filesystem::temp_directory_path() / ("aps5-pipeline-store-replay-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(root); + struct Cleanup { + std::filesystem::path directory; + ~Cleanup() { + ShaderRecompiler::ShaderDiskCache::Shutdown(); + std::error_code error; + std::filesystem::remove_all(directory, error); + } + } cleanup{root}; +#ifdef _WIN32 + _putenv_s("ANYPS5_NO_SHADER_CACHE", "0"); + _wputenv_s(L"ANYPS5_SHADER_CACHE_DIR", root.c_str()); + _putenv_s("APS5_PIPELINE_JOURNAL_THREADS", "1"); +#else + setenv("ANYPS5_NO_SHADER_CACHE", "0", 1); + setenv("ANYPS5_SHADER_CACHE_DIR", root.string().c_str(), 1); + setenv("APS5_PIPELINE_JOURNAL_THREADS", "1", 1); +#endif + Require(PipelineJournalEnabled(), "the journal is disabled with a shader cache directory set"); + mock = MockVulkan{}; + auto context = mockContext(); + context.device = makeHandle(); + context.graphicsPipelineLibrary = true; + context.pipelineLinkTimeOptimization = false; + context.limits.framebufferNoAttachmentsSampleCounts = VK_SAMPLE_COUNT_1_BIT; + ShaderRecompiler::RecompileResult vertex, fragment; + vertex.spirv = makeModule({}); + fragment.spirv = makeModule({.fragment = true}); + vertex.variantId = 401; + fragment.variantId = 402; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}}; + auto cache = std::make_unique(context); + context.descriptorCache = cache.get(); + const auto layouts = DescribeSetLayouts(context, shaders); + const auto resources = layouts.View(); + constexpr std::uint64_t fingerprint = 0x5eed'0000'0000'0002ull; + { + auto absent = StartPipelineJournalReplay(context, fingerprint, {}); + Require(absent != nullptr, "the first replay did not start"); + absent->Wait(); + } + constexpr std::uint32_t configurations = 1032; + for (std::uint32_t configuration = 0; configuration < configurations; ++configuration) { + VertexInputLayout input; + input.bindings.push_back({0, 4u * (configuration + 1u), VK_VERTEX_INPUT_RATE_VERTEX}); + static_cast(CachedPipeline(context, State{}, input, resources, shaders)); + } + FlushPipelineJournal(); + ClearCachedPipelines(context.device); + Require(PipelineStoreTotals().cached == 0, "clearing the device left pipelines in the store"); + auto replay = StartPipelineJournalReplay(context, fingerprint, {}); + Require(replay != nullptr, "the replay did not start"); + replay->Wait(); + const auto totals = PipelineStoreTotals(); + Require(totals.cached == configurations, "a replay of " + std::to_string(configurations) + " recorded pipelines kept " + std::to_string(totals.cached) + " in the store (bound " + std::to_string(totals.bound) + ")"); + ClearCachedPipelines(context.device); + context.descriptorCache = nullptr; + cache.reset(); + Require(mock.live == 0, "pipeline store replay growth tests leaked Vulkan objects"); +} + void pipelineJournalTests(bool longPath = false, bool extended = false) { using namespace AgcDriver::Graphics; using Stage = ShaderRecompiler::ShaderStage; @@ -11810,6 +11921,16 @@ int main(int argc, char** argv) { std::cout << "pipeline library clear waits for active part builds\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--pipeline-store-replay-growth") { + pipelineStoreReplayGrowthTests(); + std::cout << "a replayed journal longer than the store bound stays resident\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--pipeline-store-growth") { + pipelineStoreGrowthTests(std::getenv("APS5_PIPELINE_STORE_ENTRIES") != nullptr); + std::cout << "pipeline store bound follows the evicted pipelines that come back\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--pipeline-store-lazy-buffer-pool") { pipelineStoreLazyBufferPoolTests(); std::cout << "cached pipelines survive lazy buffer pool creation\n"; diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index a86232068f..b8aa5e4e13 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -49,6 +49,7 @@ #endif #include #include +#include #else #include #include @@ -66,6 +67,7 @@ #include #include #include +#include #include #include #include @@ -329,6 +331,7 @@ public: } return false; }; + memoryBudget = hasExtension(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME); auto bytes = AgcDriver::QueryBdaByteFeatures(context.physical, function("vkGetPhysicalDeviceFeatures2"), available); auto address = AgcDriver::QueryBdaFeatures(context.physical, function("vkGetPhysicalDeviceFeatures2"), available); const auto queues = function("vkGetPhysicalDeviceQueueFamilyProperties"); @@ -523,6 +526,21 @@ public: return properties; } void WaitQueue() const { Check(context.Function("vkQueueWaitIdle")(context.queue), "vkQueueWaitIdle"); } + struct HeapUsage { + std::uint64_t deviceLocal = 0; + std::uint64_t host = 0; + }; + std::optional MemoryUsage() const { + if (!memoryBudget) return std::nullopt; + VkPhysicalDeviceMemoryBudgetPropertiesEXT budget{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2, &budget}; + function("vkGetPhysicalDeviceMemoryProperties2")(context.physical, &properties); + HeapUsage usage; + for (std::uint32_t heap = 0; heap < properties.memoryProperties.memoryHeapCount; ++heap) { + ((properties.memoryProperties.memoryHeaps[heap].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 ? usage.deviceLocal : usage.host) += budget.heapUsage[heap]; + } + return usage; + } private: template @@ -552,6 +570,7 @@ private: bool graphics = false; std::mutex settleMutex; bool nullDescriptors = false; + bool memoryBudget = false; }; using Kind = Recorder::ReadKind; @@ -9825,6 +9844,98 @@ bool vertexTargetRangeTests(const Device& device, Recorder& recorder, bool impor return true; } +std::uint64_t processPrivateBytes() { +#ifdef _WIN32 + PROCESS_MEMORY_COUNTERS_EX counters{}; + counters.cb = sizeof(counters); + if (!K32GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast(&counters), sizeof(counters))) return 0; + return counters.PrivateUsage; +#else + std::ifstream status("/proc/self/status"); + std::string line; + while (std::getline(status, line)) { + if (line.rfind("RssAnon:", 0) == 0) return std::stoull(line.substr(8)) * 1024u; + } + return 0; +#endif +} + +// What one graphics pipeline the store keeps costs: `count` pipelines, each with its own fragment +// module (so its own fragment shader library), built through CachedPipeline and held, after their +// optimized relinks; process private bytes and VK_EXT_memory_budget heap usage before and after, +// and after the store is cleared. +int pipelineStoreMemoryBench(const Device& device, std::uint32_t count) { + if (!device.Graphics() || count == 0) { + std::cout << "skipped, the pipeline store memory bench requires a graphics queue and at least one pipeline\n"; + return 77; + } + std::lock_guard gpu(GpuMutex()); + const auto& context = device.GetContext(); + struct Cleanup { + VkDevice device; + ~Cleanup() { ClearCachedPipelines(device); } + } cleanup{context.device}; + ClearCachedPipelines(context.device); + ShaderRecompiler::RecompileResult vertex; + vertex.variantId = 21; + vertex.spirv = std::vector(std::begin(TRIANGLE_vert_SPV), std::end(TRIANGLE_vert_SPV)); + std::vector fragments(count + 1u); + for (std::uint32_t index = 0; index <= count; ++index) { + fragments[index].variantId = 1000u + index; + fragments[index].spirv = std::vector(std::begin(TRIANGLE_frag_SPV), std::end(TRIANGLE_frag_SPV)); + fragments[index].spirv[2] = 0x50535430u + index; + } + State state{}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.hasColorTarget = true; + state.color.format = VK_FORMAT_R8G8B8A8_UNORM; + state.colors.push_back(state.color); + state.blend.colorWriteMask = 15; + state.blends.push_back(state.blend); + const std::array first{CompiledShader{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, CompiledShader{ShaderRecompiler::ShaderStage::Fragment, &fragments[count], 0}}; + ShaderResources resources(context, first, state.color, static_cast(state.colors.size()), 0, 0); + const VertexInputLayout input; + std::vector> held; + held.reserve(count + 1u); + const auto build = [&](std::uint32_t index) { + const std::array shaders{CompiledShader{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, CompiledShader{ShaderRecompiler::ShaderStage::Fragment, &fragments[index], 0}}; + held.push_back(CachedPipeline(context, state, input, resources, shaders, VK_IMAGE_LAYOUT_GENERAL)); + }; + struct Sample { + std::uint64_t privateBytes; + std::optional heaps; + }; + const auto sample = [&] { + WaitForOptimizedPipelines(context.device); + return Sample{processPrivateBytes(), device.MemoryUsage()}; + }; + build(count); + const auto before = sample(); + const auto started = std::chrono::steady_clock::now(); + for (std::uint32_t index = 0; index < count; ++index) build(index); + const auto built = std::chrono::steady_clock::now(); + const auto after = sample(); + const auto linked = std::chrono::steady_clock::now(); + const auto totals = PipelineStoreTotals(); + held.clear(); + ClearCachedPipelines(context.device); + const auto cleared = sample(); + const auto mib = [](std::uint64_t later, std::uint64_t earlier) { return (static_cast(later) - static_cast(earlier)) / (1024.0 * 1024.0); }; + const auto perPipeline = [&](std::uint64_t later, std::uint64_t earlier) { return (static_cast(later) - static_cast(earlier)) / 1024.0 / count; }; + const char* linking = !context.graphicsPipelineLibrary ? "whole pipelines" : context.pipelineLinkTimeOptimization ? "libraries and optimized relinks" : "libraries, fast links only"; + std::cout << std::fixed << std::setprecision(1); + std::cout << "Pipeline store memory: " << count << " graphics pipelines (one fragment module each, " << linking << "), " << totals.cached << " in the store, built in " + << std::chrono::duration(built - started).count() << " s, relinked in " << std::chrono::duration(linked - built).count() << " s\n"; + std::cout << " process private bytes: +" << mib(after.privateBytes, before.privateBytes) << " MiB = " << perPipeline(after.privateBytes, before.privateBytes) << " KiB per pipeline; after clearing the store " << mib(cleared.privateBytes, before.privateBytes) << " MiB above the start\n"; + if (before.heaps && after.heaps && cleared.heaps) { + std::cout << " VK_EXT_memory_budget heap usage: device-local +" << mib(after.heaps->deviceLocal, before.heaps->deviceLocal) << " MiB = " << perPipeline(after.heaps->deviceLocal, before.heaps->deviceLocal) << " KiB per pipeline, host +" << mib(after.heaps->host, before.heaps->host) << " MiB = " << perPipeline(after.heaps->host, before.heaps->host) + << " KiB per pipeline; after clearing the store device-local " << mib(cleared.heaps->deviceLocal, before.heaps->deviceLocal) << " MiB, host " << mib(cleared.heaps->host, before.heaps->host) << " MiB above the start\n"; + } else { + std::cout << " VK_EXT_memory_budget unavailable: no device heap usage\n"; + } + return 0; +} + void pipelineCacheTests(const Device& device, bool benchmark) { std::thread establishThreadedProcess([] {}); establishThreadedProcess.join(); @@ -11036,6 +11147,7 @@ int main(int argc, char** argv) { pipelineLibraryTests(device); return 0; } + if ((argc == 2 || argc == 3) && std::string_view(argv[1]) == "--benchmark-pipeline-store-memory") return pipelineStoreMemoryBench(device, argc == 3 ? static_cast(std::stoul(argv[2])) : 4096u); if (argc == 2 && (std::string_view(argv[1]) == "--benchmark-pipeline-cache" || std::string_view(argv[1]) == "--pipeline-cache-only")) { pipelineCacheTests(device, std::string_view(argv[1]) == "--benchmark-pipeline-cache"); return 0; diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 6c47cfa197..af559b3d56 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -124,7 +124,7 @@ flowchart LR - [`Recompiler.cpp`](../../core/shader/recompiler/Recompiler.cpp) runs the stages in this order. With `ANYPS5_ENABLE_SPIRV_TOOLS`, the SPIR-V is also validated and optimized with SPIRV-Tools. - `ShaderRecompiler::Recompile` keeps compiled variants in memory, and `ShaderDiskCache` stores them on disk so later runs reuse them. Before the writable cache, a miss is also looked up in read-only prebuilt packs and loose entry folders under `shader_cache/prebuilt`; pack entries are ordinary encoded cache entries behind a sorted key-hash index, validated by magic, compiler source version and index checksum when mapped, and a miss or rejected entry falls through to the writable cache and the compiler. Cache format 35 also stores the finite stencil-reference set, its replay-safety proof and reserved push-data word in the artifact and binding layouts. Entries from formats 31 to 34 are rejected. -- The Vulkan pipelines are journaled as well ([`PipelineJournal.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp)): a graphics pipeline the store builds (`CachedPipeline`) or a compute pipeline a dispatch builds is appended to `pipelines.journal` next to the shader cache entries, as the materialized result of each stage (the specialized SPIR-V, bindings and push constants, through `ShaderDiskCache::EncodeResult`) plus the recipe the pipeline was built from (`GraphicsPipelineRecipe`: the stages' identities, vertex input layout, descriptor set layout keys and the state fields the `Pipeline` constructor reads, `CanonicalPipelineState`). When a `VulkanDevice` is created, the journal is loaded and its pipelines are built on a pool of `hardware_concurrency() / 2` threads (`APS5_PIPELINE_JOURNAL_THREADS`) while the title loads. The load parses the file a record at a time and keeps it open, and a replay thread reads an entry's modules from it when it builds that entry (a record that no longer reads back as loaded rejects the entry), so the replay holds the modules of the entries being built rather than the file, so the first draw or dispatch of a configuration seen on an earlier run finds its pipeline resident. The pipeline stores key on `RecompileResult::Identity`, a hash of the module words, specialization constants, subgroup width and bindings that is the same on every run (`SharedSpirv::ContentHash`, computed once per shared module), rather than on the process-lifetime variant ids, and a graphics pipeline is built outside the store's mutex, so the replay threads and the queue workers build in parallel. A draw that arrives before its replayed entry builds the pipeline itself; the first inserted wins. A replayed entry enters the store at its least recently used end and a full store takes none, so the title's own pipelines are never evicted for a replayed one, and the optimized relink of a replayed pipeline (`OptimizedPipeline::Schedule`) waits for the first draw that binds it, so the relink thread serves the title's pipelines first. A device created without a window starts its replay at its first draw or dispatch rather than at creation, since `Driver::AttachWindow` usually replaces it before the title draws. The journal records a pipeline once (modules and recipes are deduplicated by content), keeps at most `APS5_PIPELINE_JOURNAL_MAX_MIB` (1024), replays the first `APS5_PIPELINE_JOURNAL_ENTRIES` recorded (1024, the pipeline store's bound, `APS5_PIPELINE_STORE_ENTRIES`; a run reaches those first), and is disabled with `APS5_NO_PIPELINE_JOURNAL=1` or without a shader cache directory. The file header names the device (`PipelineJournalDeviceFingerprint`, the properties the driver's pipeline cache file is named by), so a journal written for another GPU or driver is started over rather than replayed; appends and reads hold `pipelines.journal.lock` against another process sharing the directory, and a device cancels its replay before it is replaced (`PrepareForReplacement`) or destroyed, so a replaced headless device never inserts entries the window's device then sweeps. `APS5_PROFILE_DRAW=1` prints a `[pipeline-journal]` summary when the replay finishes and at teardown. +- The Vulkan pipelines are journaled as well ([`PipelineJournal.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp)): a graphics pipeline the store builds (`CachedPipeline`) or a compute pipeline a dispatch builds is appended to `pipelines.journal` next to the shader cache entries, as the materialized result of each stage (the specialized SPIR-V, bindings and push constants, through `ShaderDiskCache::EncodeResult`) plus the recipe the pipeline was built from (`GraphicsPipelineRecipe`: the stages' identities, vertex input layout, descriptor set layout keys and the state fields the `Pipeline` constructor reads, `CanonicalPipelineState`). When a `VulkanDevice` is created, the journal is loaded and its pipelines are built on a pool of `hardware_concurrency() / 2` threads (`APS5_PIPELINE_JOURNAL_THREADS`) while the title loads. The load parses the file a record at a time and keeps it open, and a replay thread reads an entry's modules from it when it builds that entry (a record that no longer reads back as loaded rejects the entry), so the replay holds the modules of the entries being built rather than the file, so the first draw or dispatch of a configuration seen on an earlier run finds its pipeline resident. The pipeline stores key on `RecompileResult::Identity`, a hash of the module words, specialization constants, subgroup width and bindings that is the same on every run (`SharedSpirv::ContentHash`, computed once per shared module), rather than on the process-lifetime variant ids, and a graphics pipeline is built outside the store's mutex, so the replay threads and the queue workers build in parallel. A draw that arrives before its replayed entry builds the pipeline itself; the first inserted wins. A replayed entry enters the store at its least recently used end and a full store takes none, so the title's own pipelines are never evicted for a replayed one, and the optimized relink of a replayed pipeline (`OptimizedPipeline::Schedule`) waits for the first draw that binds it, so the relink thread serves the title's pipelines first. A device created without a window starts its replay at its first draw or dispatch rather than at creation, since `Driver::AttachWindow` usually replaces it before the title draws. The journal records a pipeline once (modules and recipes are deduplicated by content), keeps at most `APS5_PIPELINE_JOURNAL_MAX_MIB` (1024), replays the last `APS5_PIPELINE_JOURNAL_ENTRIES` recorded (4096, the pipeline store's largest bound: a replayed pipeline that finds the store full grows its bound from 1024 instead of being dropped), and is disabled with `APS5_NO_PIPELINE_JOURNAL=1` or without a shader cache directory. The file header names the device (`PipelineJournalDeviceFingerprint`, the properties the driver's pipeline cache file is named by), so a journal written for another GPU or driver is started over rather than replayed; appends and reads hold `pipelines.journal.lock` against another process sharing the directory, and a device cancels its replay before it is replaced (`PrepareForReplacement`) or destroyed, so a replaced headless device never inserts entries the window's device then sweeps. `APS5_PROFILE_DRAW=1` prints a `[pipeline-journal]` summary when the replay finishes and at teardown. - A compute shader whose LDS plus its lock dword exceeds the host's `maxComputeSharedMemorySize` is not given a `Workgroup` array. `Recompile` sets `ShaderInfo::sharedMemoryBytes` for it and the SPIR-V backend addresses the LDS in a coherent storage buffer at descriptor set 1 (`WorkgroupMemoryDescriptorSet`), with a slice of `RecompileResult::workgroupMemoryDwords` per workgroup, indexed by `WorkgroupID` and `NumWorkGroups`. `VulkanDevice` keeps one such buffer, sized from the dispatch dimensions, and binds it after the resource set; programs that fit keep using shared memory. - `Driver::RegisterShader` splits shader preparation in two. `PlanRegistered` runs on the registering guest thread: it validates the registers and entry point, decodes the code and builds the recompile request, so invalid registrations still fail there. The RDNA to IR translation and resource plan (`PrepareShader`) run on a background pool of `hardware_concurrency() / 2` detached threads (at least 2, 64 MiB stacks). - A thread that needs the GPU mutex, the committer's next item or the end of the committer's queue spins before it blocks (`HandoffSpin.hpp`): `GpuMutexType::lock` retries `try_lock` for the window, the committer polls its item count, and `DrawPipeline::Drain` polls it for twice the window, so a hand-off that completes within the window costs no kernel wake-up. The window is 150 us when the process may run on more than 8 logical processors (its affinity mask, via `CpuTopology`) and 0 otherwise, where the spinning threads would compete with the holder; `APS5_HANDOFF_SPIN_US=` sets it (0 turns it off, at most 10000). Ordering and the lock's semantics are unchanged. diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 3a728911cd..ea96f961b9 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -351,11 +351,11 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_PIPELINE_DRAIN_FILLS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp` | | runs queue-0 buffer fills through the draw pipeline to keep their order; any value stores them in place instead | | `APS5_PIPELINE_DRAIN_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | answers a queue-0 wait from the draw pipeline's own pending labels while it is busy; any value drains the pipeline at the wait | | `APS5_PIPELINE_DRAIN_SUBMISSIONS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | lets a submission end without submitting its recorded work, so it joins a later submit with the pipeline's queue; any value drains the pipeline and submits at the end of the submission | -| `APS5_PIPELINE_JOURNAL_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp` | 1024 | number of journal entries loaded at most; a longer journal keeps its last entries, the ones recorded last | +| `APS5_PIPELINE_JOURNAL_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp` | 4096 | number of journal entries loaded at most; a longer journal keeps its last entries, the ones recorded last | | `APS5_PIPELINE_JOURNAL_MAX_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp` | 1024 | size cap in MiB on the pipeline journal; the file is rewritten down to three quarters of the cap when it is reached, keeping the newest pipelines and the modules they name and leaving room for later records | | `APS5_PIPELINE_JOURNAL_THREADS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp` | | threads that replay the journal's pipelines, 0 disables the replay; unset uses half the hardware threads, at least one | | `APS5_PIPELINE_STATS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | captures and prints driver statistics for each new pipeline, requiring `VK_KHR_pipeline_executable_properties`; `1` enables | -| `APS5_PIPELINE_STORE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp` | 1024 | LRU bound on the entries of the pipeline store; an entry is evicted only once no recorded draw holds it | +| `APS5_PIPELINE_STORE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp` | adaptive | fixes the LRU bound on the entries of the pipeline store, which otherwise starts at 1024 and doubles up to 4096 for a journal replay that does not fit and while evicted pipelines keep coming back; an entry is evicted only once no recorded draw holds it | | `APS5_PM4_RING` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp` | 16 | number of submissions the PM4 ring keeps for dumps, from 0 to 4096; 0 disables the ring | | `APS5_POLL_REAP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | reaps completions on every poll tick | | `APS5_POLL_TRY_EACH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | tries the poll service on every tick instead of at the poll interval | diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 36535fa9b4..9504679a01 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -114,7 +114,7 @@ - [AGC buffer allocations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) retry host/device out-of-memory once after reclaiming idle pool buffers and fully unused slab blocks. Live buffers and blocks with live slots are retained. Live images and other retained resources have no global memory bound; allocation failure still throws when reclaim cannot satisfy the request, except for the device buffers and images that the "Host placement after a device-memory refusal" entry places in host memory. - [Storage-cache pressure and idle device buffers](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp): the one-second idle observation interval is a retention choice, not a console rule; buffers reused less often can be recreated under pressure. Before a storage-cache eviction, independently allocated device-tier buffers unused throughout that interval can be released, oldest first, for the cache's shortfall below its existing ceiling. Live buffers, host tiers and slab allocations are not reclaimed this way. Reclamation requires exactly one device-local heap because retained buffers do not record their heap. The heap budget is read again after an actual release; a delayed driver report can still cause an eviction. This is not a shared budget for all retained GPU resources, and its effect on titles with intermittent large buffer use is unmeasured. - [Host placement after a device-memory refusal](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) (libSceAgcDriver): when the driver refuses a device-local allocation with `VK_ERROR_OUT_OF_DEVICE_MEMORY` after the pool's trim and the texture-cache release, device buffers and images (textures, storage textures, block, repacked, depth, depth plane and stencil copies, render, depth and multisampled targets and attachment proxies) are allocated in a host memory type their requirements allow. A host-placed device buffer leaves the pool on release. A host-placed image keeps that placement until it is destroyed and is never moved back to device memory, so every access to it crosses PCIe for its lifetime. The placements are counted in a rate-limited `[bufferpool]` line with the cumulative MiB, not the resident bytes. The 1x1 denormal clear probe still allocates device memory directly and throws on a refusal. -- [Pipeline journal](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp) (libSceAgcDriver): the journal is append-only until it reaches `APS5_PIPELINE_JOURNAL_MAX_MIB`, when it is rewritten to three quarters of the cap with the newest pipelines and their modules; the rewrite streams kept records; a replay thread reads an entry's modules when it builds that entry. Only the last `APS5_PIPELINE_JOURNAL_ENTRIES` pipelines are replayed, and the store evicts the least recently used entry past `APS5_PIPELINE_STORE_ENTRIES`: a title with more configurations rebuilds the rest at the draw. A compute pipeline is replayed over the set layout `DescribeSetLayouts` derives from the result's bindings, checked against its `ShaderResources` layout only under `APS5_VERIFY_SET_LAYOUTS=1`. Recording and replay are best effort: a record whose write fails is lost and an entry whose build fails is skipped, both counted in the `[pipeline-journal]` line. The file header's format number guards the recipe codec and the store key. +- [Pipeline journal](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp) (libSceAgcDriver): resident graphics pipelines are bounded by count, not bytes: the store grows from 1024 to 4096 entries for a replay that does not fit and for evicted pipelines that keep coming back, and the last `APS5_PIPELINE_JOURNAL_ENTRIES` (4096) records are all replayed, so a pipeline the title never asks for again costs its memory until teardown. `agc_driver_recorder_tests --benchmark-pipeline-store-memory [count]` gives a floor with the triangle shaders: about 59 KiB host and no device memory per pipeline on an RTX 4090, 100 KiB host and 8 KiB device-local on a UHD 770; larger shaders cost more. The file is rewritten to three quarters of `APS5_PIPELINE_JOURNAL_MAX_MIB` when full. A compute pipeline is replayed over the layout `DescribeSetLayouts` derives, checked only under `APS5_VERIFY_SET_LAYOUTS=1`. The header's format number guards the recipe codec and the store key. - [Graphics pipeline libraries](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp) (libSceAgcDriver): a pipeline is fast-linked at the draw and relinked with `VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT` on a background thread, and binds the optimized one from the next draw on. Draws recorded before that use the fast link, whose vertex stage keeps the outputs the fragment stage does not read: on RADV (Radeon 660M, Mesa 26.2.4) 346 of 483 Astro's Playroom vertex shaders paired with a fragment stage that reads none of them used more VGPRs (median 8 more, up to 2.5 times), and on an RTX 4090 13 of 15 PPSA36082 vertex shaders up to 68 instead of 38 (#2242). The optimized link compiles to the same code as a whole pipeline - [Fragment shader interlock pipelines](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver): a pipeline whose fragment shader declares a fragment shader interlock capability is never built from graphics pipeline libraries, on every driver, so it is compiled whole at the draw with no fast link to bridge the compile. RADV's fast link ([mesa#16653](https://gitlab.freedesktop.org/mesa/mesa/-/work_items/16653), Mesa 26.2.5, RX 7900 GRE) jumps to the PS epilog without waiting for the shader's stores, and the epilog's `done` export ends the GFX11 primitive-ordered section, so the next overlapping wave reads the old value: `agc_driver_pixel_interlock` lost read-modify-writes in 108 of 120 loaded runs under `APS5_NO_PIPELINE_LTO=1`. On an RTX 4090 (NVIDIA 617.42, Windows) the fast link kept the ordering in 240 of 240 loaded runs; other drivers are not measured. - [Prebuilt pipeline libraries](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver): each registered vertex or pixel shader in registration form gets its pre-rasterization or fragment-shader library built on one background thread, with a fixed state the draw may not use: triangle list, counter-clockwise front face, no depth clamp, depth bias or conservative rasterization, first provoking vertex, one sample. Libraries are kept until the device is torn down; on Astro's Playroom 1948 of them added 0.44 GB of peak memory on RADV. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 3a4dc187ee..b0b1b4ce87 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -318,7 +318,7 @@ Recompiled shaders and the Vulkan pipeline cache are kept in a `shader_cache/` d ### Pipeline journal -Besides the recompiled shaders, the shader cache directory holds `pipelines.journal`: every Vulkan graphics pipeline the pipeline store kept and every compute pipeline a run built, with the specialized shader modules they were built from. The next run of the title builds those pipelines on background threads while it loads, so a draw or dispatch the earlier run already made does not compile its pipeline when it is first reached. The journal belongs to the GPU and driver that wrote it, like the driver's pipeline cache file next to it: a run on another GPU or driver version starts it over, and the driver's own pipeline cache decides how fast each pipeline is rebuilt. Two runs sharing the directory take turns on `pipelines.journal.lock`; a run that cannot lock it records nothing. `APS5_NO_PIPELINE_JOURNAL=1` disables recording and replay; `APS5_PIPELINE_JOURNAL_THREADS` sets the replay threads (half the CPU threads by default; `0` records without replaying), `APS5_PIPELINE_JOURNAL_ENTRIES` how many pipelines are replayed (the first recorded, which a run reaches first) and `APS5_PIPELINE_STORE_ENTRIES` how many graphics pipelines stay resident (1024 each), and `APS5_PIPELINE_JOURNAL_MAX_MIB` (1024) the file size at which recording stops. Deleting the file loses nothing but the head start. With `APS5_PROFILE_DRAW=1` a `[pipeline-journal]` line reports what was loaded, replayed and recorded. +Besides the recompiled shaders, the shader cache directory holds `pipelines.journal`: every Vulkan graphics pipeline the pipeline store kept and every compute pipeline a run built, with the specialized shader modules they were built from. The next run of the title builds those pipelines on background threads while it loads, so a draw or dispatch the earlier run already made does not compile its pipeline when it is first reached. The journal belongs to the GPU and driver that wrote it, like the driver's pipeline cache file next to it: a run on another GPU or driver version starts it over, and the driver's own pipeline cache decides how fast each pipeline is rebuilt. Two runs sharing the directory take turns on `pipelines.journal.lock`; a run that cannot lock it records nothing. `APS5_NO_PIPELINE_JOURNAL=1` disables recording and replay; `APS5_PIPELINE_JOURNAL_THREADS` sets the replay threads (half the CPU threads by default; `0` records without replaying), `APS5_PIPELINE_JOURNAL_ENTRIES` how many pipelines are replayed (4096, the last recorded) and `APS5_PIPELINE_STORE_ENTRIES` how many graphics pipelines stay resident (by default the store grows from 1024 up to 4096 to hold the replay and the pipelines that keep coming back), and `APS5_PIPELINE_JOURNAL_MAX_MIB` (1024) the file size at which recording stops. Deleting the file loses nothing but the head start. With `APS5_PROFILE_DRAW=1` a `[pipeline-journal]` line reports what was loaded, replayed and recorded. ### Draw pipeline From a938dd8f20f5b752f273c9d42c91a3d803e76b29 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 21 Oct 2026 21:23:57 +0000 Subject: [PATCH 293/428] feat(libc): write the called imports to APS5_TRACE_CALLS_FILE every 10 seconds A traced run that ends without a crash or an abort never reports which imports it called. With APS5_TRACE_CALLS_FILE set, the first traced import starts a thread that rewrites that file every 10 seconds with every import called so far, most called first. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/include/CallTrace.hpp | 1 + .../libc/src/specifics/x86_64/CallTrace.cpp | 45 +++++++++++++++++++ core/libs/tests/GuestCallTrace.cpp | 16 +++++++ docs/dev/ENVIRONMENT.md | 1 + docs/dev/TRACING.md | 1 + docs/user/USAGE.md | 7 +++ 6 files changed, 71 insertions(+) diff --git a/core/libs/prx/libc/include/CallTrace.hpp b/core/libs/prx/libc/include/CallTrace.hpp index b53c1ba96e..430700f0b5 100644 --- a/core/libs/prx/libc/include/CallTrace.hpp +++ b/core/libs/prx/libc/include/CallTrace.hpp @@ -23,6 +23,7 @@ void* TraceImport_nid_no_patch(void* target, const char* name, const char* libra std::size_t CopyCallTrace_nid_no_patch(CallTraceEntry* entries, std::size_t capacity); std::size_t CopyThreadCallTrace_nid_no_patch(std::uint64_t thread, CallTraceEntry* entries, std::size_t capacity); std::size_t CopyCallCounts_nid_no_patch(CallTraceCount* entries, std::size_t capacity, std::uint64_t* total); +bool WriteCallCounts_nid_no_patch(const char* path); } #endif diff --git a/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp b/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp index c81b91a39a..694896a3b0 100644 --- a/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp +++ b/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp @@ -14,15 +14,21 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include +#include +#include #include #include #include #include #include #include +#include #include +#include +#include extern "C" void CallTraceEnter_nid_no_patch(); @@ -71,6 +77,39 @@ bool Selected(std::string_view library) { return false; } +bool WriteCallCounts(const std::filesystem::path& path) { + const std::size_t recorded = g_recordCount.load(std::memory_order_acquire); + const ImportRecord* records = g_records.load(std::memory_order_acquire); + std::vector> called; + for (std::size_t index = 0; index < recorded; ++index) { + const std::uint64_t calls = records[index].calls.load(std::memory_order_relaxed); + if (calls != 0) called.emplace_back(calls, &records[index]); + } + std::stable_sort(called.begin(), called.end(), [](const auto& left, const auto& right) { return left.first > right.first; }); + std::ofstream file(path, std::ios::binary | std::ios::trunc); + for (const auto& [calls, record] : called) file << calls << ' ' << record->library << ' ' << record->name << '\n'; + file.flush(); + return static_cast(file); +} + +void StartCallCountWriter() { +#ifdef _WIN32 + const wchar_t* value = _wgetenv(L"APS5_TRACE_CALLS_FILE"); + if (value == nullptr || *value == L'\0') return; + std::filesystem::path path(value); +#else + const char* value = std::getenv("APS5_TRACE_CALLS_FILE"); + if (value == nullptr || *value == '\0') return; + std::filesystem::path path(value); +#endif + std::thread([path = std::move(path)] { + for (;;) { + std::this_thread::sleep_for(std::chrono::seconds(10)); + WriteCallCounts(path); + } + }).detach(); +} + std::uint64_t CurrentThread() { #ifdef _WIN32 return GetCurrentThreadId(); @@ -334,6 +373,7 @@ void* TraceImport_nid_no_patch(void* target, const char* name, const char* libra #ifdef __linux__ InstallAbortReport(); #endif + StartCallCountWriter(); } #ifdef __linux__ SnapshotModules(); @@ -386,6 +426,11 @@ std::size_t CopyThreadCallTrace_nid_no_patch(std::uint64_t thread, CallTraceEntr return count; } +bool WriteCallCounts_nid_no_patch(const char* path) { + if (path == nullptr || *path == '\0') return false; + return WriteCallCounts(std::filesystem::path(reinterpret_cast(path))); +} + std::size_t CopyCallCounts_nid_no_patch(CallTraceCount* entries, std::size_t capacity, std::uint64_t* total) { if (total != nullptr) *total = g_sequence.load(std::memory_order_acquire); if (entries == nullptr || capacity == 0) return 0; diff --git a/core/libs/tests/GuestCallTrace.cpp b/core/libs/tests/GuestCallTrace.cpp index 6fc77ae658..6721aaddac 100644 --- a/core/libs/tests/GuestCallTrace.cpp +++ b/core/libs/tests/GuestCallTrace.cpp @@ -12,8 +12,12 @@ #include #include #include +#include +#include +#include #include #include +#include #include #include @@ -199,4 +203,16 @@ int main(int argc, char** argv) { Require(owned == Window); Require(CopyCallCounts_nid_no_patch(counts, 1, &total) == 1 && total == 8 + Workers * Calls); Require(counts[0].calls == 1 + Workers * Calls && std::strcmp(counts[0].name, "Integers") == 0); + + const auto path = std::filesystem::temp_directory_path() / ("guest_call_trace_counts_" + std::to_string(CurrentThread()) + ".txt"); + const auto utf8 = path.u8string(); + Require(WriteCallCounts_nid_no_patch(reinterpret_cast(utf8.c_str()))); + std::ifstream written(path, std::ios::binary); + const std::string text{std::istreambuf_iterator(written), std::istreambuf_iterator()}; + written.close(); + Require(text == std::to_string(1 + Workers * Calls) + " libTraced.prx Integers\n3 libTraced.prx Blocks\n2 libTraced Doubles\n1 libOther.prx Mixed\n1 libOther Variadic\n"); + const auto unwritable = (path / "file").u8string(); + Require(!WriteCallCounts_nid_no_patch(reinterpret_cast(unwritable.c_str()))); + Require(!WriteCallCounts_nid_no_patch(nullptr) && !WriteCallCounts_nid_no_patch("")); + std::filesystem::remove(path); } diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 3a728911cd..2a7d6b0a9a 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -565,6 +565,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_PIN_WAIT_MS` | integer | `core/libs/prx/libc/src/GuestAllocations.cpp` | 60000 | milliseconds to wait for a pinned allocation lease before failing | | `APS5_TRACE_AFFINITY` | presence | `core/libs/prx/libc/include/CpuTopology.hpp` | | prints a line naming the role, tid and mask of every thread pinning | | `APS5_TRACE_CALLS` | string | `core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp` | | comma-separated libraries (or all) whose guest calls are traced | +| `APS5_TRACE_CALLS_FILE` | string | `core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp` | | file rewritten every 10 seconds with each traced import called so far and its number of calls | | `APS5_TRACE_EXIT` | presence | `core/libs/prx/libc/src/Process.cpp` | | prints a backtrace of the caller of exit, _Exit and the process-exit path | | `APS5_TRACE_SSE4A` | presence | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | prints each emulated SSE4a instruction once per rip | | `APS5_WATCH_ADDR` | integer | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | absolute hex address whose writes are reported after its page is committed | diff --git a/docs/dev/TRACING.md b/docs/dev/TRACING.md index 8cb84bb4a4..324c5279bb 100644 --- a/docs/dev/TRACING.md +++ b/docs/dev/TRACING.md @@ -12,6 +12,7 @@ Set a switch before starting the emulator. Unless noted, any value enables it. M | `APS5_TRACE_AUDIOOUT` | Audio output port parameters when a port is opened. | | `APS5_TRACE_AUDIOOUT2` | AudioOut2 context and port parameters, attributes, pushes, queue polls and periodic queue summaries. | | `APS5_TRACE_CALLS` | On Windows and Linux, records calls to the imports selected by this comma-separated library list (or `all`); crash and abort reports (abort only on Linux) include the last 64 calls and most-called imports. The executable must be converted with `--trace-imports`; see [Usage](../user/USAGE.md#tracing-system-library-calls). | +| `APS5_TRACE_CALLS_FILE` | With `APS5_TRACE_CALLS`, a file rewritten every 10 seconds with every traced import called so far, most called first: number of calls, library and import name. | | `APS5_TRACE_EXIT` | Guest `exit` and `_Exit` calls and process-exit backtraces. | | `APS5_TRACE_FIBER` | Fiber initialization, switches and returns. | | `APS5_TRACE_MEMORY` | Direct-memory allocation, mapping and protection operations. | diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 6c27fdd74f..73405083d9 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -355,6 +355,13 @@ most called traced imports (1 calls in total): 1 libkernel.prx k+AXqu2-eBc ``` +To list every import a title calls during a run that ends without a crash, also set `APS5_TRACE_CALLS_FILE` to a file path. Every 10 seconds the file is rewritten with one line per import called so far, most called first: the number of calls, the library and the import name. It survives a run that is killed, up to its last rewrite. + +``` +245557 libc.prx gQX+4GDQjpM#guest +3866 libSceAgc.prx 57labkp+rSQ +``` + Limits are listed in the [libc](../dev/debt/libc.md) and [relinker](../dev/debt/relinker.md) technical debt. ### Performance overlay From 9c73da714bc61f0b06dbc78a144e087d6affb25e Mon Sep 17 00:00:00 2001 From: milescoviello Date: Thu, 22 Oct 2026 09:22:49 +0000 Subject: [PATCH 294/428] fix(build): link the ATRAC9 gradient test with libm atrac9_gradient_tests is a C program linked against the static atrac9 library, whose band_extension.c calls pow. The test never listed libm. The default Linux build links tests statically with --gc-sections, which drops the unreferenced band extension code before pow has to resolve, so the missing library went unnoticed. A shared link, as in an ANYPS5_ENABLE_ASAN build, resolves symbols before collecting sections: ld finds pow only in the libm.so.6 that libasan pulls in and stops with "DSO missing from command line", so the test is never built and ctest reports it as not run. Link libm outside Windows, as atrac9_config_data_tests next to it already does. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAjm.native/CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/prx/libSceAjm.native/CMakeLists.txt b/core/libs/prx/libSceAjm.native/CMakeLists.txt index 20f6cf1f20..5dbf8f7d66 100644 --- a/core/libs/prx/libSceAjm.native/CMakeLists.txt +++ b/core/libs/prx/libSceAjm.native/CMakeLists.txt @@ -36,6 +36,9 @@ if(BUILD_TESTING) add_test_executable(atrac9_gradient_tests tests/Atrac9Gradient.c ${LIBATRAC9_DIR}/bit_reader.c) target_include_directories(atrac9_gradient_tests PRIVATE ${LIBATRAC9_PATCHED_DIR} ${LIBATRAC9_DIR}) target_link_libraries(atrac9_gradient_tests PRIVATE atrac9) + if(NOT WIN32) + target_link_libraries(atrac9_gradient_tests PRIVATE m) + endif() target_compile_options(atrac9_gradient_tests PRIVATE -ffunction-sections -fdata-sections) if(APPLE) target_link_options(atrac9_gradient_tests PRIVATE LINKER:-dead_strip) From 2362e377907e0d5bfdcd6cd5c20a50cb847ddf76 Mon Sep 17 00:00:00 2001 From: hypermist Date: Sun, 25 Oct 2026 13:25:41 +1300 Subject: [PATCH 295/428] refactor(libSceFont): use the library headers in the font style information test Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestFontStyleInformation.cpp | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/core/libs/tests/GuestFontStyleInformation.cpp b/core/libs/tests/GuestFontStyleInformation.cpp index b3e25044ba..21443be90b 100644 --- a/core/libs/tests/GuestFontStyleInformation.cpp +++ b/core/libs/tests/GuestFontStyleInformation.cpp @@ -1,4 +1,6 @@ #include "prx/libSceFont/include/FontDriver.hpp" +#include "prx/libSceFont/libSceFont.hpp" +#include "prx/libSceFontFt/libSceFontFt.hpp" #include "tests/FontFixture.hpp" #include #include @@ -11,18 +13,6 @@ #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 sceFontSupportExternalFonts(FontLibrary, std::uint32_t, std::uint32_t); -int APS5_VABI sceFontOpenFontMemory(FontLibrary, const void*, std::uint32_t, const FontOpenDetail*, FontHandle*); -int APS5_VABI sceFontCloseFont(FontHandle); -int APS5_VABI sceFontGetFontStyleInformation(FontHandle, FontStyleInformation*); -const void* APS5_VABI sceFontSelectLibraryFt(int); -} - static void Check(bool value, int line) { if (!value) { std::fprintf(stderr, "Font style information check failed at line %d\n", line); From 83cb98a9c60752a4394f7e0390f603c3dd8aae7a Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 25 Oct 2026 02:30:51 +0200 Subject: [PATCH 296/428] test(libSceRtc): round-trip a fixed tick through the host's local time The local-time round trip converted the current tick. During the hour that repeats when daylight saving time ends, that local time occurs twice, so converting it back can give the other instant and the check fails. Convert the test's fixed leap-day tick instead. Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestRtc.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 10a2900e2f..c84bbc7a09 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -762,10 +762,11 @@ int main(int argc, char** argv) { RtcTick now{}; Require(sceRtcGetCurrentTick(&now) == 0 && now.tick > leapDayTick); + const RtcTick fixed{leapDayTick}; RtcTick local{}; RtcTick back{}; - Require(sceRtcConvertUtcToLocalTime(&now, &local) == 0); - Require(sceRtcConvertLocalTimeToUtc(&local, &back) == 0 && back.tick == now.tick); + Require(sceRtcConvertUtcToLocalTime(&fixed, &local) == 0); + Require(sceRtcConvertLocalTimeToUtc(&local, &back) == 0 && back.tick == fixed.tick); char rfc2822[32]; tick.tick = leapDayTick; From e5fcbb775d3c1128434f5a104a4b8a6807b88930 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Sat, 24 Oct 2026 21:10:27 -0300 Subject: [PATCH 297/428] fix(check-compile): enforce syntax mode --- CMakeLists.txt | 9 ++ docs/dev/TechnicalDebt.md | 1 + tools/check_compile.py | 45 +++++-- tools/tests/test_check_compile.py | 4 +- tools/tests/test_check_compile_modes.py | 172 ++++++++++++++++++++++++ 5 files changed, 221 insertions(+), 10 deletions(-) create mode 100644 tools/tests/test_check_compile_modes.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 4726a34e12..f6558d1031 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -273,6 +273,15 @@ if(BUILD_TESTING) add_test(NAME performance_compare COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_performance_compare.py) set_tests_properties(performance_compare PROPERTIES TIMEOUT 20) + set(check_compile_modes_source "${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_compile_modes.py") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${check_compile_modes_source}") + file(STRINGS "${check_compile_modes_source}" check_compile_modes_cases REGEX "^ def test_") + foreach(test_case IN LISTS check_compile_modes_cases) + string(REGEX REPLACE "^ def (test_[A-Za-z0-9_]+).*" "\\1" test_case "${test_case}") + add_test(NAME check_compile_modes_${test_case} COMMAND ${Python3_EXECUTABLE} + "${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_compile_modes.py" CheckCompileModeTests.${test_case}) + set_tests_properties(check_compile_modes_${test_case} PROPERTIES TIMEOUT 30) + endforeach() endif() endif() diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index ee2a2dfd75..dde1757815 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -29,3 +29,4 @@ Throughout the project, every function at every stage either **does exactly what - `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows. - [Quick syntax checks](../../tools/check_compile.py) reject unexpanded `@response` arguments rather than parse compiler-specific quoting and risk retaining output flags. Expand response files in `compile_commands.json` before checking; the Ninja fallback expands them with `ninja -t compdb -x`. +- [Quick syntax checks](../../tools/check_compile.py) do not model every compiler option's operand count. A preprocessing mode following an unrecognized option without a joined value is rejected rather than risk removing that option's operand. Compiler wrappers with their own option grammar and forwarded preprocessing modes are not covered. diff --git a/tools/check_compile.py b/tools/check_compile.py index c2c0befc3a..14de111946 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -13,6 +13,16 @@ from pathlib import Path CPP = (".cpp", ".cc", ".cxx", ".c") DROP = ("-c", "-MD", "-MMD", "-MP") DROP_WITH_VALUE = ("-o", "-MT", "-MF", "-MQ", "-MJ") +PREPROCESSING = ("-E", "--preprocess", "-M", "--dependencies", "-MM", "--user-dependencies", + "-MG", "--print-missing-file-dependencies", "-P", "--no-line-commands", + "-C", "-CC", "-dM", "-dD", "-dI") +WITH_VALUE = ("-I", "-L", "-F", "-B", "-D", "-U", "-x", "-include", "--include", "-imacros", "--imacros", + "--define-macro", "--undefine-macro", "-iquote", "-isystem", "-idirafter", "-iprefix", + "-iwithprefix", "-iwithprefixbefore", "-isysroot", "--sysroot", "-target", "--target", + "-arch", "-std", "-T", "-u", "-e", "--param", "-Xclang", "-Xpreprocessor", "-Xassembler", "-Xlinker") +WITHOUT_VALUE = (*DROP, *PREPROCESSING, "-S", "-fsyntax-only", "-Werror", "-Wall", "-Wextra", + "-pedantic", "-pedantic-errors", "-pthread", "-pipe", "-v") +JOINED_OPTIONS = ("-I", "-L", "-F", "-B", "-D", "-U", "-O", "-g", "-W") def capture(command, cwd=None, encoding="utf-8"): @@ -155,26 +165,45 @@ def syntax_arguments(arguments, directory, source): if argument.startswith("@"): raise ValueError(f"unexpanded response file {argument[1:]}; provide expanded compile arguments before checking syntax") wanted = source.resolve() - trimmed = [] - index = 0 + trimmed = [arguments[0]] + index = 1 + positional = False + syntax_added = False while index < len(arguments): argument = arguments[index] - if argument in DROP_WITH_VALUE: - index += 2 - continue - if argument in DROP or argument.startswith(("-MT", "-MF", "-MQ", "-MJ")): + if not syntax_added and argument.startswith("-"): + trimmed.append("-fsyntax-only") + syntax_added = True + if argument == "--": + positional = True + trimmed.append(argument) index += 1 continue - if not argument.startswith("-"): + if not positional and argument in (*DROP_WITH_VALUE, *WITH_VALUE): + if index + 1 == len(arguments): + raise ValueError(f"missing operand for {argument}") + if argument in WITH_VALUE: + trimmed.extend(arguments[index:index + 2]) + index += 2 + continue + if not positional and (argument in (*DROP, *PREPROCESSING) or argument.startswith(("-MT", "-MF", "-MQ", "-MJ"))): + index += 1 + continue + if positional or not argument.startswith("-"): candidate = Path(argument) if not candidate.is_absolute(): candidate = directory / candidate if candidate.resolve() == wanted: index += 1 continue + elif (argument not in WITHOUT_VALUE and not argument.startswith(JOINED_OPTIONS) and "=" not in argument and index + 1 < len(arguments) + and arguments[index + 1] in PREPROCESSING): + raise ValueError(f"unknown operand count for {argument} before {arguments[index + 1]}") trimmed.append(argument) index += 1 - trimmed += ["-fsyntax-only", str(wanted)] + if not syntax_added: + trimmed.append("-fsyntax-only") + trimmed.append(str(wanted)) return trimmed diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py index c2f6f77643..6bbbbf5570 100644 --- a/tools/tests/test_check_compile.py +++ b/tools/tests/test_check_compile.py @@ -290,7 +290,7 @@ class CheckCompileTests(unittest.TestCase): result = self.tool("unit.cpp") self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(json.loads(seen.read_text(encoding="utf-8")), - [*flags, "-fsyntax-only", str(source)]) + ["-fsyntax-only", *flags, str(source)]) def test_nothing_checked_is_a_failure(self): self.manifest([]) @@ -503,7 +503,7 @@ class CheckCompileTests(unittest.TestCase): result = self.tool(source.name) self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(json.loads(seen.read_text(encoding="utf-8")), - [*flags, "-fsyntax-only", str(source)]) + ["-fsyntax-only", *flags, str(source)]) def test_invalid_argument_arrays_are_rejected_without_a_traceback(self): source = self.source("unit.cpp") diff --git a/tools/tests/test_check_compile_modes.py b/tools/tests/test_check_compile_modes.py new file mode 100644 index 0000000000..ac0b7d5810 --- /dev/null +++ b/tools/tests/test_check_compile_modes.py @@ -0,0 +1,172 @@ +import json +import os +import shlex +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +TOOL = ROOT / "tools/check_compile.py" + + +class CheckCompileModeTests(unittest.TestCase): + def compilers(self): + values = list(dict.fromkeys(value for value in (shutil.which("gcc"), shutil.which("clang")) if value)) + if not values: + self.skipTest("needs GCC or Clang") + return values + + def run_tool(self, root, build, source, arguments, field): + if not (root / ".git").exists(): + subprocess.run(["git", "init", "-q", str(root)], capture_output=True, check=True) + command = (subprocess.list2cmdline(arguments) if os.name == "nt" else + " ".join(shlex.quote(value) for value in arguments)) + entry = {"directory": str(build), "file": str(source), + field: arguments if field == "arguments" else command} + (build / "compile_commands.json").write_text(json.dumps([entry]), encoding="utf-8") + before = {path: path.read_bytes() for path in root.rglob("*") if path.is_file()} + result = subprocess.run([sys.executable, "-I", str(TOOL), "--build", str(build), str(source)], + cwd=root, capture_output=True, text=True, encoding="utf-8", check=False) + self.assertEqual({path for path in root.rglob("*") if path.is_file()}, set(before), result.stderr) + for path, data in before.items(): + self.assertEqual(path.read_bytes(), data, path.name) + self.assertNotIn("Traceback", result.stderr) + return result + + def check_modes(self, variants, dependency=False, configure=None, invalid=None, marker="syntax_must_be_checked"): + for compiler in self.compilers(): + for flags in variants: + for field in ("arguments", "command"): + with self.subTest(compiler=compiler, flags=flags, field=field), tempfile.TemporaryDirectory(prefix="syntax mode ") as name: + root = Path(name).resolve() + build = root / "build" + build.mkdir() + source = root / "unit.c" + object_file, dependency_file = build / "unit.o", build / "unit.d" + prefix = "#if REQUIRED != 42\n#error definition was lost\n#endif\n" + suffix = "" + extra = list(flags) + if configure is not None: + additions, opening, suffix = configure(root, build, source) + extra += additions + prefix = opening + prefix + valid = prefix + "int main(void) { return 0; }\n" + suffix + invalid_text = prefix + (invalid or "int syntax_must_be_checked = ;\n") + suffix + compile_flags = [] if any(flag in flags for flag in ("-E", "--preprocess", "-M", "--dependencies", "-MM", "--user-dependencies")) else ["-c"] + arguments = [compiler, "-D", "REQUIRED=42", *extra, *compile_flags, str(source), "-o", str(object_file)] + if dependency: + arguments += ["-MF", str(dependency_file)] + source.write_text(valid, encoding="utf-8") + control = subprocess.run(arguments, cwd=build, capture_output=True, text=True, check=False) + self.assertEqual(control.returncode, 0, control.stderr) + object_file.write_bytes(b"existing object output\n") + dependency_file.write_bytes(b"existing dependency output\n") + for text, expected in ((valid, 0), (invalid_text, 1)): + source.write_text(text, encoding="utf-8") + result = self.run_tool(root, build, source, arguments, field) + self.assertEqual(result.returncode, expected, result.stdout + result.stderr) + if expected: + self.assertIn(marker, result.stderr) + else: + self.assertIn("1 translation units compile", result.stdout) + + def test_preprocessing_modes_reach_syntax_errors(self): + self.check_modes((("-E",), ("--preprocess",))) + + def test_dependency_modes_reach_syntax_errors(self): + self.check_modes((("-M",), ("--dependencies",)), dependency=True) + + def test_user_dependency_modes_reach_syntax_errors(self): + self.check_modes((("-MM",), ("--user-dependencies",)), dependency=True) + + def test_generated_header_mode_does_not_hide_missing_headers(self): + self.check_modes((("-M", "-MG"), ("--dependencies", "--print-missing-file-dependencies")), + dependency=True, invalid='#include "missing_generated_header.h"\n', + marker="missing_generated_header.h") + + def test_preprocessor_output_companions_do_not_block_syntax_checks(self): + self.check_modes((("-Werror", "-E", "-P"), ("-Werror", "--preprocess", "--no-line-commands"))) + + def test_macro_dump_modes_still_check_syntax(self): + self.check_modes((("-Werror", "-E", "-dM"), ("-Werror", "-E", "-dD"))) + + def test_comment_output_modes_still_check_syntax(self): + self.check_modes((("-Werror", "-E", "-C"), ("-Werror", "-E", "-CC"))) + + def test_joined_options_before_preprocessing_modes_are_preserved(self): + self.check_modes((("-DANOTHER_MACRO", "-E"), ("-UUNUSED_MACRO", "-E"), ("-O2", "-Wall", "-E"))) + + def test_include_directory_values_can_look_like_modes(self): + def configure(root, build, source): + directory = build / "-E" + directory.mkdir() + (directory / "only_here.h").write_text("#define FROM_INCLUDE 1\n", encoding="utf-8") + return ["-I", "-E"], '#include "only_here.h"\n#ifndef FROM_INCLUDE\n#error include lost\n#endif\n', "" + self.check_modes(((),), configure=configure) + + def test_library_directory_values_can_look_like_modes(self): + self.check_modes((("-L", "-E"),)) + + def test_forced_include_values_can_look_like_modes(self): + def configure(root, build, source): + (build / "-M").write_text("#define FROM_INCLUDE 1\n", encoding="utf-8") + return ["-include", "-M"], "#ifndef FROM_INCLUDE\n#error include lost\n#endif\n", "" + self.check_modes(((),), configure=configure) + + def test_source_path_used_as_an_option_value_is_preserved(self): + def configure(root, build, source): + return ["-include", str(source)], "#ifndef INCLUDED_ONCE\n#define INCLUDED_ONCE\n", "#endif\n" + self.check_modes(((),), configure=configure) + + def test_missing_values_fail_before_a_syntax_flag_is_injected(self): + for compiler in self.compilers(): + for option in ("-I", "-L", "-include", "-x", "-o", "-MF"): + for field in ("arguments", "command"): + with self.subTest(compiler=compiler, option=option, field=field), tempfile.TemporaryDirectory(prefix="syntax missing ") as name: + root = Path(name).resolve() + build = root / "build" + build.mkdir() + source = root / "unit.c" + source.write_text("int main(void) { return 0; }\n", encoding="utf-8") + result = self.run_tool(root, build, source, [compiler, option], field) + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("missing", result.stderr.lower()) + + def test_unknown_options_still_fail(self): + for compiler in self.compilers(): + for field in ("arguments", "command"): + with self.subTest(compiler=compiler, field=field), tempfile.TemporaryDirectory(prefix="syntax unknown ") as name: + root = Path(name).resolve() + build = root / "build" + build.mkdir() + source = root / "unit.c" + source.write_text("int main(void) { return 0; }\n", encoding="utf-8") + option = "-funknown-syntax-mode-test" + result = self.run_tool(root, build, source, [compiler, "-E", option, "-c", str(source)], field) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn(option, result.stderr) + + def test_unknown_operand_counts_are_rejected_before_mode_rewriting(self): + compiler = shutil.which("clang") + if compiler is None: + self.skipTest("needs Clang") + for field in ("arguments", "command"): + with self.subTest(field=field), tempfile.TemporaryDirectory(prefix="syntax arity ") as name: + root = Path(name).resolve() + build = root / "build" + build.mkdir() + source = root / "unit.c" + source.write_text("int main(void) { return 0; }\n", encoding="utf-8") + arguments = [compiler, "-fdebug-compilation-dir", "-E", "-c", str(source), "-o", str(build / "unit.o")] + control = subprocess.run(arguments, cwd=build, capture_output=True, text=True, check=False) + self.assertEqual(control.returncode, 0, control.stderr) + result = self.run_tool(root, build, source, arguments, field) + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("unknown operand count", result.stderr) + + +if __name__ == "__main__": + unittest.main() From cea2536efe63901c36c3c831360f19f4775d976a Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Sat, 24 Oct 2026 21:12:22 -0300 Subject: [PATCH 298/428] fix(check-compile): preserve positional inputs --- tools/check_compile.py | 6 +++++- tools/tests/test_check_compile_modes.py | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/tools/check_compile.py b/tools/check_compile.py index 14de111946..10dc33086a 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -174,7 +174,7 @@ def syntax_arguments(arguments, directory, source): if not syntax_added and argument.startswith("-"): trimmed.append("-fsyntax-only") syntax_added = True - if argument == "--": + if not positional and argument == "--": positional = True trimmed.append(argument) index += 1 @@ -196,6 +196,10 @@ def syntax_arguments(arguments, directory, source): if candidate.resolve() == wanted: index += 1 continue + if positional: + trimmed.append(str(candidate.resolve())) + index += 1 + continue elif (argument not in WITHOUT_VALUE and not argument.startswith(JOINED_OPTIONS) and "=" not in argument and index + 1 < len(arguments) and arguments[index + 1] in PREPROCESSING): raise ValueError(f"unknown operand count for {argument} before {arguments[index + 1]}") diff --git a/tools/tests/test_check_compile_modes.py b/tools/tests/test_check_compile_modes.py index ac0b7d5810..f9bedaafff 100644 --- a/tools/tests/test_check_compile_modes.py +++ b/tools/tests/test_check_compile_modes.py @@ -99,6 +99,29 @@ class CheckCompileModeTests(unittest.TestCase): def test_joined_options_before_preprocessing_modes_are_preserved(self): self.check_modes((("-DANOTHER_MACRO", "-E"), ("-UUNUSED_MACRO", "-E"), ("-O2", "-Wall", "-E"))) + def test_explicit_cpp_language_is_preserved(self): + self.check_modes((("-E", "-x", "c++"),)) + + def test_clang_end_of_options_preserves_mode_named_inputs(self): + compiler = shutil.which("clang") + if compiler is None: + self.skipTest("needs Clang") + for field in ("arguments", "command"): + with self.subTest(field=field), tempfile.TemporaryDirectory(prefix="syntax positional ") as name: + root = Path(name).resolve() + build = root / "build" + build.mkdir() + source = root / "unit.c" + source.write_text("int main(void) { return 0; }\n", encoding="utf-8") + other = build / "-E" + arguments = [compiler, "-x", "c", "--", "-E", str(source)] + for text, expected in (("int extra;\n", 0), ("int positional_syntax_error = ;\n", 1)): + other.write_text(text, encoding="utf-8") + result = self.run_tool(root, build, source, arguments, field) + self.assertEqual(result.returncode, expected, result.stdout + result.stderr) + if expected: + self.assertIn("positional_syntax_error", result.stderr) + def test_include_directory_values_can_look_like_modes(self): def configure(root, build, source): directory = build / "-E" From 699337abfd9646da9bf90afbf90ef554033610bb Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Sat, 24 Oct 2026 21:16:21 -0300 Subject: [PATCH 299/428] fix(check-compile): keep one syntax action --- CMakeLists.txt | 18 +++++------ tools/check_compile.py | 2 +- tools/tests/test_check_compile_modes.py | 40 ++++++++++++++++++++----- 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f6558d1031..9d44c2b48b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -246,6 +246,15 @@ if(BUILD_TESTING) add_test(NAME conventions_bom_shebang COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_bom_shebang.py) set_tests_properties(conventions_bom_shebang PROPERTIES TIMEOUT 20) + set(check_compile_modes_source "${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_compile_modes.py") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${check_compile_modes_source}") + file(STRINGS "${check_compile_modes_source}" check_compile_modes_cases REGEX "^ def test_") + foreach(test_case IN LISTS check_compile_modes_cases) + string(REGEX REPLACE "^ def (test_[A-Za-z0-9_]+).*" "\\1" test_case "${test_case}") + add_test(NAME check_compile_modes_${test_case} COMMAND ${Python3_EXECUTABLE} + "${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_compile_modes.py" CheckCompileModeTests.${test_case}) + set_tests_properties(check_compile_modes_${test_case} PROPERTIES TIMEOUT 30) + endforeach() add_test(NAME env_reference COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_env_reference.py) set_tests_properties(env_reference PROPERTIES TIMEOUT 20) @@ -273,15 +282,6 @@ if(BUILD_TESTING) add_test(NAME performance_compare COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_performance_compare.py) set_tests_properties(performance_compare PROPERTIES TIMEOUT 20) - set(check_compile_modes_source "${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_compile_modes.py") - set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${check_compile_modes_source}") - file(STRINGS "${check_compile_modes_source}" check_compile_modes_cases REGEX "^ def test_") - foreach(test_case IN LISTS check_compile_modes_cases) - string(REGEX REPLACE "^ def (test_[A-Za-z0-9_]+).*" "\\1" test_case "${test_case}") - add_test(NAME check_compile_modes_${test_case} COMMAND ${Python3_EXECUTABLE} - "${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_compile_modes.py" CheckCompileModeTests.${test_case}) - set_tests_properties(check_compile_modes_${test_case} PROPERTIES TIMEOUT 30) - endforeach() endif() endif() diff --git a/tools/check_compile.py b/tools/check_compile.py index 10dc33086a..8a0083c473 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -11,7 +11,7 @@ from contextlib import nullcontext from pathlib import Path CPP = (".cpp", ".cc", ".cxx", ".c") -DROP = ("-c", "-MD", "-MMD", "-MP") +DROP = ("-c", "-MD", "-MMD", "-MP", "-fsyntax-only", "-fno-syntax-only") DROP_WITH_VALUE = ("-o", "-MT", "-MF", "-MQ", "-MJ") PREPROCESSING = ("-E", "--preprocess", "-M", "--dependencies", "-MM", "--user-dependencies", "-MG", "--print-missing-file-dependencies", "-P", "--no-line-commands", diff --git a/tools/tests/test_check_compile_modes.py b/tools/tests/test_check_compile_modes.py index f9bedaafff..eb4424be28 100644 --- a/tools/tests/test_check_compile_modes.py +++ b/tools/tests/test_check_compile_modes.py @@ -13,8 +13,8 @@ TOOL = ROOT / "tools/check_compile.py" class CheckCompileModeTests(unittest.TestCase): - def compilers(self): - values = list(dict.fromkeys(value for value in (shutil.which("gcc"), shutil.which("clang")) if value)) + def compilers(self, names=("gcc", "clang")): + values = list(dict.fromkeys(value for value in map(shutil.which, names) if value)) if not values: self.skipTest("needs GCC or Clang") return values @@ -36,8 +36,8 @@ class CheckCompileModeTests(unittest.TestCase): self.assertNotIn("Traceback", result.stderr) return result - def check_modes(self, variants, dependency=False, configure=None, invalid=None, marker="syntax_must_be_checked"): - for compiler in self.compilers(): + def check_modes(self, variants, dependency=False, configure=None, invalid=None, marker="syntax_must_be_checked", compiler_names=("gcc", "clang"), wrap=False, trailing=False): + for compiler in self.compilers(compiler_names): for flags in variants: for field in ("arguments", "command"): with self.subTest(compiler=compiler, flags=flags, field=field), tempfile.TemporaryDirectory(prefix="syntax mode ") as name: @@ -56,7 +56,15 @@ class CheckCompileModeTests(unittest.TestCase): valid = prefix + "int main(void) { return 0; }\n" + suffix invalid_text = prefix + (invalid or "int syntax_must_be_checked = ;\n") + suffix compile_flags = [] if any(flag in flags for flag in ("-E", "--preprocess", "-M", "--dependencies", "-MM", "--user-dependencies")) else ["-c"] - arguments = [compiler, "-D", "REQUIRED=42", *extra, *compile_flags, str(source), "-o", str(object_file)] + driver = [compiler] + if wrap: + wrapper = root / "wrapper.py" + wrapper.write_text("import subprocess, sys\n" + "if sys.argv[2:].count('-fsyntax-only') != 1: sys.exit(78)\n" + "sys.exit(subprocess.run(sys.argv[1:]).returncode)\n", encoding="utf-8") + driver = [sys.executable, str(wrapper), compiler] + arguments = [*driver, "-D", "REQUIRED=42", *([] if trailing else extra), + *compile_flags, str(source), *(extra if trailing else []), "-o", str(object_file)] if dependency: arguments += ["-MF", str(dependency_file)] source.write_text(valid, encoding="utf-8") @@ -93,11 +101,29 @@ class CheckCompileModeTests(unittest.TestCase): def test_macro_dump_modes_still_check_syntax(self): self.check_modes((("-Werror", "-E", "-dM"), ("-Werror", "-E", "-dD"))) + def test_include_dump_mode_still_checks_syntax(self): + self.check_modes((("-Werror", "-E", "-dI"),)) + + def test_real_compiler_wrapper_keeps_one_syntax_mode(self): + self.check_modes((("-fsyntax-only", "-E"),), wrap=True) + + def test_trailing_preprocessing_mode_still_checks_syntax(self): + self.check_modes((("-E",),), trailing=True) + def test_comment_output_modes_still_check_syntax(self): self.check_modes((("-Werror", "-E", "-C"), ("-Werror", "-E", "-CC"))) - def test_joined_options_before_preprocessing_modes_are_preserved(self): - self.check_modes((("-DANOTHER_MACRO", "-E"), ("-UUNUSED_MACRO", "-E"), ("-O2", "-Wall", "-E"))) + def test_joined_definitions_before_preprocessing_modes_are_preserved(self): + self.check_modes((("-DANOTHER_MACRO", "-E"),)) + + def test_joined_undefinitions_before_preprocessing_modes_are_preserved(self): + self.check_modes((("-UUNUSED_MACRO", "-E"),)) + + def test_optimization_and_warning_options_before_preprocessing_modes_are_preserved(self): + self.check_modes((("-O2", "-Wall", "-E"),)) + + def test_gcc_negative_syntax_mode_cannot_enable_output(self): + self.check_modes((("-fno-syntax-only",),), compiler_names=("gcc",)) def test_explicit_cpp_language_is_preserved(self): self.check_modes((("-E", "-x", "c++"),)) From 8a6f7843a26359f16c394b46b79251cef49c5f27 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Sat, 24 Oct 2026 21:24:56 -0300 Subject: [PATCH 300/428] test(check-compile): split missing operand cases --- CMakeLists.txt | 10 +++++----- tools/tests/test_check_compile_modes.py | 10 ++++++++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d44c2b48b..813ad77f6e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -241,11 +241,6 @@ if(BUILD_TESTING) add_test(NAME pad_timeline COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pad_timeline.py) set_tests_properties(pad_timeline PROPERTIES TIMEOUT 20) - add_test(NAME trace_digest COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_trace_digest.py) - add_test(NAME conventions_bom_shebang COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_bom_shebang.py) - set_tests_properties(conventions_bom_shebang PROPERTIES TIMEOUT 20) set(check_compile_modes_source "${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_compile_modes.py") set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${check_compile_modes_source}") file(STRINGS "${check_compile_modes_source}" check_compile_modes_cases REGEX "^ def test_") @@ -255,6 +250,11 @@ if(BUILD_TESTING) "${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_compile_modes.py" CheckCompileModeTests.${test_case}) set_tests_properties(check_compile_modes_${test_case} PROPERTIES TIMEOUT 30) endforeach() + add_test(NAME trace_digest COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_trace_digest.py) + add_test(NAME conventions_bom_shebang COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_bom_shebang.py) + set_tests_properties(conventions_bom_shebang PROPERTIES TIMEOUT 20) add_test(NAME env_reference COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_env_reference.py) set_tests_properties(env_reference PROPERTIES TIMEOUT 20) diff --git a/tools/tests/test_check_compile_modes.py b/tools/tests/test_check_compile_modes.py index eb4424be28..9b4200ae86 100644 --- a/tools/tests/test_check_compile_modes.py +++ b/tools/tests/test_check_compile_modes.py @@ -170,9 +170,15 @@ class CheckCompileModeTests(unittest.TestCase): return ["-include", str(source)], "#ifndef INCLUDED_ONCE\n#define INCLUDED_ONCE\n", "#endif\n" self.check_modes(((),), configure=configure) - def test_missing_values_fail_before_a_syntax_flag_is_injected(self): + def test_missing_path_values_fail_before_a_syntax_flag_is_injected(self): + self.check_missing_values(("-I", "-L", "-include")) + + def test_missing_language_and_output_values_fail_before_a_syntax_flag_is_injected(self): + self.check_missing_values(("-x", "-o", "-MF")) + + def check_missing_values(self, options): for compiler in self.compilers(): - for option in ("-I", "-L", "-include", "-x", "-o", "-MF"): + for option in options: for field in ("arguments", "command"): with self.subTest(compiler=compiler, option=option, field=field), tempfile.TemporaryDirectory(prefix="syntax missing ") as name: root = Path(name).resolve() From 52f36c8aa2455bef8e27294f12977e71f83be543 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Thu, 22 Oct 2026 03:24:38 +0000 Subject: [PATCH 301/428] fix(agc): keep a mipmapped DCC color target's keys on its resident image DecodeColorBuffer dropped the DCC address of a color target with more than one mip, so the target's resident image had no keys: it was uploaded from guest memory under the game's fast-clear keys, and no draw into it marked them uncompressed. The game's textures over the same chain name the keys, so they kept sampling the clear (0000 reads as (0,0,0,0)) whatever was drawn. MXGP 26, Borderlands 4 and Forspoken render such targets. - ColorTarget.chainDccAddress keeps the keys of a 2D single-layer chain; dccAddress stays 0, so the per-draw single-mip key handling is unchanged. - SurfaceForTarget gives the resident surface those keys, and the image then treats them as a multi-mip storage image does: a uniform clear is cleared on the GPU at upload, and the first write marks every key uncompressed. - A draw recipe's key check uses the surface's keys. - sizeof(State) is 840: the recipe codec and CanonicalPipelineState do not copy the field. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/include/State.hpp | 1 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 2 +- .../Graphics/src/PipelineJournal.cpp | 2 +- .../libSceAgcDriver/Graphics/src/State.cpp | 13 ++++-- .../prx/libSceAgcDriver/tests/Graphics.cpp | 7 +++ .../tests/execution/DccKeysFirstWrite.cpp | 45 +++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 7 files changed, 65 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 4e7174f582..294c4bda9b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -45,6 +45,7 @@ struct ColorTarget { ShaderRecompiler::ColorExportPacking packing = ShaderRecompiler::ColorExportPacking::None; // DCC metadata of a compressed target (CB_COLOR_INFO DCC_ENABLE), or 0 (see DccMetadata.hpp). std::uint64_t dccAddress = 0; + std::uint64_t chainDccAddress = 0; bool dccAlphaOnMsb = false; bool dccPipeAligned = false; std::uint64_t cmaskAddress = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index a91fc9d2ff..0af56987c9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -3279,7 +3279,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con timer.phase(PhaseSetup); if (state.colors[index].fmaskAddress != 0) GuardOneFragmentFmask(context, state.colors[index]); auto stored = recipe.targets[index].lock(); - if (stored == nullptr || !StorageImageCached(context, stored.get()) || !StorageImageServesKeys(*stored, state.colors[index].dccAddress)) return miss(DrawRecipeMiss::TargetGone); + if (stored == nullptr || !StorageImageCached(context, stored.get()) || !StorageImageServesKeys(*stored, SurfaceForTarget(state.colors[index]).dccAddress)) return miss(DrawRecipeMiss::TargetGone); auto resident = refreshResidentTarget(context, state, state.colors[index], outcome, profile, [&] { if (state.colors[index].arrayLastSlice != 0) stored->RefreshLayers(state.colors[index].arraySlice, 1); else stored->Refresh(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index 7eba2c79ac..a177efd44b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -38,7 +38,7 @@ static_assert(sizeof(VkStencilOpState) == 28, "VkStencilOpState changed: update static_assert(sizeof(VkVertexInputBindingDescription) == 12, "VkVertexInputBindingDescription changed: update the recipe codec"); static_assert(sizeof(VkVertexInputAttributeDescription) == 16, "VkVertexInputAttributeDescription changed: update the recipe codec"); #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) -static_assert(sizeof(State) == 832, "State changed: update CanonicalPipelineState and the recipe codec"); +static_assert(sizeof(State) == 840, "State changed: update CanonicalPipelineState and the recipe codec"); static_assert(sizeof(ShaderStages) == 112, "ShaderStages changed: update the recipe codec"); static_assert(sizeof(ShaderRecompiler::MeshConfiguration) == 40, "MeshConfiguration changed: update the recipe codec"); static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "TessellationConfiguration changed: update the recipe codec"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index de94232f92..da78c29b18 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1912,7 +1912,7 @@ GuestTextureResource SurfaceForTarget(const ColorTarget& color) { surface.dstSelY = 5; surface.dstSelZ = 6; surface.dstSelW = 7; - surface.dccAddress = color.dccAddress; + surface.dccAddress = color.dccAddress != 0 ? color.dccAddress : color.chainDccAddress; surface.dccAlphaOnMsb = color.dccAlphaOnMsb; surface.dccPipeAligned = color.dccPipeAligned; return surface; @@ -2081,9 +2081,10 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo Require(color.fmaskAddress != 0, "a compressed one-fragment color target without an FMASK address is unsupported"); } if ((info & 0x10000000u) != 0) { + const auto dccHigh = find(cx, 0x3a8 + slot); + const auto dccBase = ((dccHigh == cx.end() ? 0ull : static_cast(dccHigh->second & 0xffu)) << 40u) | (static_cast(read(cx, 0x325 + stride)) << 8u); if (maxMip == 0) { - const auto dccHigh = find(cx, 0x3a8 + slot); - color.dccAddress = ((dccHigh == cx.end() ? 0ull : static_cast(dccHigh->second & 0xffu)) << 40u) | (static_cast(read(cx, 0x325 + stride)) << 8u); + color.dccAddress = dccBase; color.dccAlphaOnMsb = DccAlphaOnMsb(color.format, swap); color.dccPipeAligned = ((attrib3 >> 30u) & 1u) != 0; if (color.samples > 1) { @@ -2091,11 +2092,15 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo color.dccControl = read(cx, 0x31e + stride); if (!allowMultisampleDcc) Require((color.dccControl & 0x3ff80u) == 0, "multisampled DCC color transforms or lossy modes are not implemented"); } + } else if (!volume && color.arrayLastSlice == 0 && slice == 0) { + color.chainDccAddress = dccBase; + color.dccAlphaOnMsb = DccAlphaOnMsb(color.format, swap); + color.dccPipeAligned = ((attrib3 >> 30u) & 1u) != 0; } else { static bool reported = false; if (!reported) { reported = true; - std::fprintf(stderr, "[gpu] DCC keys of mipmapped color targets are ignored\n"); + std::fprintf(stderr, "[gpu] DCC keys of mipmapped array and 3D color targets are ignored\n"); } } } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 95080d73f9..95e0818c41 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3134,6 +3134,13 @@ void metadataPassTests() { auto mipmapped = queue; mipmapped.context[0x3b0] |= 1u << 28u; expectFailure([&] { DecodeColorMetadataPass(mipmapped); }, "mipmapped DCC"); + const auto chain = AgcDriver::Graphics::DecodeColorBuffer(mipmapped.context, 0); + const auto chainSurface = AgcDriver::Graphics::SurfaceForTarget(chain); + Require(chain.dccAddress == 0 && chainSurface.mipCount == 2 && chainSurface.dccAddress == keysAddress, "the surface of a mipmapped DCC color target lost its keys"); + auto mipmappedArray = mipmapped; + mipmappedArray.context[0x3b8] |= 1u; + mipmappedArray.context[0x31b] = 1u << 13u; + Require(AgcDriver::Graphics::SurfaceForTarget(AgcDriver::Graphics::DecodeColorBuffer(mipmappedArray.context, 0)).dccAddress == 0, "a mipmapped DCC array color target took keys whose layers are not modeled"); const AgcDriver::Graphics::Context context{}; const auto texels = [&](std::uint32_t value) { for (std::size_t offset = 0; offset < colorMemory.size(); offset += 4) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp index 6faa64b11e..5dc96d71c0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" @@ -7,7 +8,9 @@ #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" @@ -826,6 +829,45 @@ void RunSharedImport(AgcDriver::VulkanDevice& device, std::uint8_t* block) { } +void RunMipmappedTarget(AgcDriver::VulkanDevice& device, std::uint8_t* block) { +#ifndef _WIN32 + constexpr std::uint32_t ChainSide = 64; + constexpr std::uint32_t ChainLevels = 7; + auto* texels = block; + auto* keys = block + KeysOffset; + const auto surface = AddressOf(texels); + const auto meta = AddressOf(keys); + std::memset(texels, 0x55, SurfaceBytes); + std::memset(keys, 0x00, KeyBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + AgcDriver::Registers cx; + cx[0x318] = static_cast(surface >> 8u); + cx[0x390] = static_cast(surface >> 40u); + cx[0x31b] = 0; + cx[0x31c] = 0x10028028u; + cx[0x31d] = 0; + cx[0x3b0] = ((ChainLevels - 1u) << 28u) | ((ChainSide - 1u) << 14u) | (ChainSide - 1u); + cx[0x3b8] = 0x9000000u | (TileRenderTarget64KB << 14u) | (1u << 30u); + cx[0x325] = static_cast(meta >> 8u); + cx[0x3a8] = static_cast(meta >> 40u); + const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + const auto resource = AgcDriver::Graphics::SurfaceForTarget(color); + Require(color.mipCount == ChainLevels && color.dccAddress == 0 && resource.dccAddress == meta, "the resident surface of a mipmapped DCC color target has no keys"); + const auto bytes = AgcDriver::Graphics::DescribeSurface(resource).guestBytes; + Require(bytes <= SurfaceBytes, "the mip chain does not fit the test surface"); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + const auto image = AgcDriver::Graphics::CachedStorageSurface(context, resource); + Require(image->UploadedKeys() == DccKeys::Clear0000, "the resident image of a 0000-cleared mipmapped target was not cleared from its keys"); + image->MarkDirty(); + Require(AgcDriver::Graphics::CurrentDccKeys(meta, bytes) == DccKeys::Uncompressed, "the first write into a mipmapped DCC target left its keys cleared"); + Require(AgcDriver::Graphics::TextureClearKeys(resource, bytes) == DccKeys::Uncompressed, "a sampled view of a rendered mipmapped DCC target still reads its clear"); +#else + static_cast(device); + static_cast(block); +#endif +} + int main() { try { #ifdef _WIN32 @@ -866,6 +908,9 @@ int main() { std::optional uniform; uniform.emplace(false); RunUniformKeysBehindKernel(*device, uniform->Data()); + std::optional chain; + chain.emplace(false); + RunMipmappedTarget(*device, chain->Data()); #ifdef _WIN32 shared.emplace(true, true); RunSharedImport(*device, shared->Data()); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 0f69cda538..7667fcd3f2 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -193,6 +193,7 @@ - [HDR output override](../../../core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp) - setting `APS5_FORCE_SDR` suppresses HDR availability reported to titles while retaining the detected display and surface capabilities. It selects the existing SDR presentation path; it does not add support for floating-point BT.2100 PQ buffers (`0xC001070722000000` and `0xC001070700000000`), whose presentation remains refused. The override does not change the host display settings. - [DCC key stores](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp) cover the console's DCC extent only for pipe-aligned DCC of single-level, single-layer 4-byte SW_64KB_R_X surfaces; other surfaces, and every texture and color target key read, cover one byte per 256 bytes. - [DCC key fills over other images](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) - a uniform fill at a live image's `dccAddress`, at least one key per 256 bytes long, is a key fill of that image even when other live images overlap it, but only while it is no longer than the image's key extent (`DccKeyCount`, the pipe-aligned count); a longer one is classified by its overlaps as before. The bound is inferred, not measured on a title: a longer fill also writes the bytes past the keys, which the overlapping image may own. Whether the title's bloom-target key fills stay within the extent was not checked (`APS5_TRACE_DCC_KEYS` prints the length of the fills classified as keys, not of the refused ones). +- [Mipmapped DCC color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - the keys of a 2D color target with more than one mip are modeled for its whole surface only, as for a multi-mip storage image. The resident image takes a uniform clear over all of them, then marks them all uncompressed at its first write. The per-draw handling of a single-mip target's keys does not apply. A draw through the copied (non-resident) path leaves the keys as they were, and the CB metadata passes still refuse such a target. Keys that hold a different state per mip, and `ClearRegister` keys (whose value is a CB register), are not modeled for such a target. The keys of mipmapped DCC array and 3D targets are ignored. - [Comp-to-single DCC keys](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp) (libSceAgcDriver) - key 0x10 (`GFX9_DCC_CLEAR_SINGLE` in Mesa `ac_descriptors.h`) uniform over a surface makes every texel the element at the start of its 256-byte block, the top-left texel of the block where radv's `build_clear_dcc_comp_to_single_shader` stores the clear color. Sampled textures and storage images read that expansion (a storage image's first write stores the keys uncompressed, as under a clear code), and a CB DCC decompress pass stores it over the texels. A fast-clear eliminate over such keys throws, as a metadata pass over mixed keys does; color targets that render over such keys use their stored texels, and textures read mixed keys, and comp-to-single keys of a format whose element does not divide 256 bytes, as stored. "Uniform" is judged over the key bytes the driver reads, one per 256 surface bytes (see DCC key stores above), so a pipe-aligned surface whose keys differ past that range is expanded as if all were comp-to-single, as for the other clear codes. Not verified on hardware - [8_SRGB color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) on a device that cannot attach and blend R8_SRGB render into an RGBA8_SRGB copy of the target, so the sRGB encode, decode and blend rounding are the device's; the console's are not measured. - [8_SRGB color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. From e0b90dc0548127d70cbf03f85cb51beae726197b Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Sat, 24 Oct 2026 21:39:26 -0300 Subject: [PATCH 302/428] test(check-compile): detect Clang's gcc alias --- tools/tests/test_check_compile_modes.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/tests/test_check_compile_modes.py b/tools/tests/test_check_compile_modes.py index 9b4200ae86..1a7e3d867b 100644 --- a/tools/tests/test_check_compile_modes.py +++ b/tools/tests/test_check_compile_modes.py @@ -123,6 +123,10 @@ class CheckCompileModeTests(unittest.TestCase): self.check_modes((("-O2", "-Wall", "-E"),)) def test_gcc_negative_syntax_mode_cannot_enable_output(self): + compiler = self.compilers(("gcc",))[0] + version = subprocess.run([compiler, "--version"], capture_output=True, text=True, check=False) + if "clang" in (version.stdout + version.stderr).lower(): + self.skipTest("GCC's negative syntax mode is unsupported by Clang") self.check_modes((("-fno-syntax-only",),), compiler_names=("gcc",)) def test_explicit_cpp_language_is_preserved(self): From 13342462a45a2f53b796ea84c84ffa5dc58708a2 Mon Sep 17 00:00:00 2001 From: hypermist Date: Sun, 25 Oct 2026 13:41:29 +1300 Subject: [PATCH 303/428] refactor(libSceFont): use the library headers in the font outline profile test Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestFontOutlineProfile.cpp | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/core/libs/tests/GuestFontOutlineProfile.cpp b/core/libs/tests/GuestFontOutlineProfile.cpp index 2cd7b4c8e9..302739a8a8 100644 --- a/core/libs/tests/GuestFontOutlineProfile.cpp +++ b/core/libs/tests/GuestFontOutlineProfile.cpp @@ -1,4 +1,6 @@ #include "prx/libSceFont/include/FontDriver.hpp" +#include "prx/libSceFont/libSceFont.hpp" +#include "prx/libSceFontFt/libSceFontFt.hpp" #include "tests/FontFixture.hpp" #include #include @@ -10,18 +12,6 @@ #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 sceFontSupportExternalFonts(FontLibrary, std::uint32_t, std::uint32_t); -int APS5_VABI sceFontOpenFontMemory(FontLibrary, const void*, std::uint32_t, const FontOpenDetail*, FontHandle*); -int APS5_VABI sceFontCloseFont(FontHandle); -int APS5_VABI sceFontGetFontGlyphsOutlineProfile(FontHandle, FontOutlineProfile*); -const void* APS5_VABI sceFontSelectLibraryFt(int); -} - static void Check(bool value, int line) { if (!value) { std::fprintf(stderr, "Font outline profile check failed at line %d\n", line); From 0297073da222302dce10aecd7597e7487dc467f8 Mon Sep 17 00:00:00 2001 From: Rookie <175737347+rooklz@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:09:09 +0100 Subject: [PATCH 304/428] fix(libc): return the calling thread from _Thrd_id as the console's libc.prx does --- core/libs/prx/libc/include/Api.hpp | 2 +- core/libs/prx/libc/src/Threads.cpp | 9 ++++----- core/libs/tests/GuestLibcThreads.cpp | 3 +++ docs/dev/debt/libc.md | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index afdb17d63c..de54164d07 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -1004,7 +1004,7 @@ void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(GuestPad* pad); void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, void** thread); int APS5_VABI _Thrd_join_nid_postfix(void* thread, int* code); int APS5_VABI _Thrd_detach_nid_postfix(void* thread); -int APS5_VABI _Thrd_id_nid_postfix(void); +void* APS5_VABI _Thrd_id_nid_postfix(void); void APS5_VABI _Call_once_nid_postfix(std::int32_t* flag, void (APS5_VABI* function)(void)); int APS5_VABI _Tss_create_nid_postfix(int* key, void (APS5_VABI* destructor)(void*)); int APS5_VABI _Tss_delete_nid_postfix(int key); diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index 8f13974d7a..ffa49e1ea7 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -488,11 +488,6 @@ int APS5_VABI _Thrd_detach_nid_postfix(void* thread) { return detach(thread) == 0 ? threadSuccess : threadError; } -int APS5_VABI _Thrd_id_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - [[noreturn]] void APS5_VABI _ZSt16_Throw_Cpp_errori_nid_postfix(int code) { ThrowCppError_nid_no_patch(code); } @@ -537,6 +532,10 @@ void* APS5_VABI _Thrd_current_nid_postfix(void) { return ThreadForwards(__func__).self(); } +void* APS5_VABI _Thrd_id_nid_postfix(void) { + return ThreadForwards(__func__).self(); +} + [[noreturn]] void APS5_VABI _Thrd_exit_nid_postfix(int code) { ThreadForwards(__func__).exit(reinterpret_cast(static_cast(static_cast(code)))); std::abort(); diff --git a/core/libs/tests/GuestLibcThreads.cpp b/core/libs/tests/GuestLibcThreads.cpp index 1d2b3d7dff..a1f0a30703 100644 --- a/core/libs/tests/GuestLibcThreads.cpp +++ b/core/libs/tests/GuestLibcThreads.cpp @@ -300,6 +300,7 @@ static void PadDetach() { static void KernelKeys() { Require(_Thrd_current_nid_postfix() == scePthreadSelf()); + Require(_Thrd_id_nid_postfix() == scePthreadSelf()); int key = -1; Require(_Tss_create_nid_postfix(&key, nullptr) == success && key >= 0); int value = 0; @@ -346,10 +347,12 @@ static void ForwardedCalls() { Require(_Tss_set_nid_postfix(8, &flag) == success && forwardLog.key == 8 && forwardLog.value == &flag); Require(_Thrd_equal_nid_postfix(&flag, &key) == 1 && forwardLog.first == &flag && forwardLog.second == &key); Require(_Thrd_current_nid_postfix() == &forwardLog.key); + Require(_Thrd_id_nid_postfix() == &forwardLog.key); Require(_Mtx_current_owns_nid_postfix(nullptr) == 1); RegisterThreadForwards_nid_no_patch(nullptr); RequireThrows([] { _Thrd_current_nid_postfix(); }); + RequireThrows([] { _Thrd_id_nid_postfix(); }); RequireThrows([] { _Tss_get_nid_postfix(0); }); } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 2d4e0fc01d..dbb046fe76 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -162,7 +162,7 @@ - [codecvt vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, NID `H-TDszhsYuY`) is thirteen zeroed `uintptr_t` slots matching `st_size` 104 on title `libc.prx` from PPSA26344 and on `libSceLibcInternal.sprx` (offset-to-top, typeinfo, two destructors, `_Incref`/`_Decref`, and seven conversion virtuals). Facet calls through the object fault until those methods are wired. - [num_put vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, NID `59oywaaZbJk`) is fourteen zeroed `uintptr_t` slots matching `st_size` 112 on the title `libc.prx` from PPSA16833 (offset-to-top, typeinfo, two destructors, `_Incref`/`_Decref`, and eight `do_put` virtuals). Facet calls through the object fault until those methods are wired. - [ios_base standard streams](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) `_Addstd` queues the stream in the standard stream table of Dinkumware [ios.cpp](https://github.com/ganboing/crt.vc12/blob/master/src/ios.cpp) (`stdstr`, `stdopens`, `_Addstd`, `_Ios_base_dtor`): it takes the first of slots 1 to 7 that is free or already holds the stream, and slot 8 (`NSTDSTR`) when none is, so slot 8 is shared by every stream past the first seven and registration never fails. It stores the 1-based slot index in the guest `IosBase::standardStream` (Dinkumware's `_Stdstr`, where 0 means not a standard stream) and counts the stream objects registered per slot; registering the same stream again reuses its slot and raises its count. Measured on a PS5 (firmware 13.60) from a homebrew payload calling the system `libSceLibcInternal` export: in a process whose own standard streams already hold slots 1 to 7, twelve distinct streams and a repeated one all get 8, written as a 64-bit value at offset 0x8 with no other byte of the object touched; the assignment of slots 1 to 7 to fresh streams is not observable there and follows VC12. The counts are `int`, not VC12's `char`, and the registration and the destructor lock the table where VC12 locks only `_Addstd`; a slot index past the table throws in the destructor instead of indexing out of bounds. The destructor decrements its slot's count and suppresses the rest of the destruction while the count stays positive, as `_Ios_base_dtor` does, and the slots of destroyed streams keep their pointer and stay reserved for that stream, as `stdstr` does. The streams themselves are destroyed by the title's own code as it registers them through `__cxa_atexit`; when the last reference closes, the unsupported stream storage and callback cleanup is still the throw it was and the locale is released as for a non-standard stream. -- [std::_Pad, _Thrd_join, _Thrd_detach, _Thrd_id](../../../core/libs/prx/libc/src/Threads.cpp) (libc) follow Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. `_Thrd_id` throws because whether the console returns `pthread_t` or a kernel thread ID and at what width is unknown; `_Launch` omits `_Cnd_do_broadcast_at_thread_exit` and calls `pthread_create` registered by libkernel with default attributes. The 32-byte pad layout (`_Go()` vtable at 0x0, `_Cnd_t` at 0x8, `_Mtx_t` at 0x10, `bool _Started` at 0x18) is assumed from VC12, unverified. Failing primitives or thread start throw the guest `std::system_error`; null arguments throw. `_Thrd_join` returns `_Thrd_error` (4) on `pthread_join` failure and truncates the exit code to `int`; `_Thrd_detach` returns `_Thrd_error` (4) on `pthread_detach` failure and throws when none is registered +- [std::_Pad, _Thrd_join, _Thrd_detach, _Thrd_id](../../../core/libs/prx/libc/src/Threads.cpp) (libc) follow Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), otherwise without console verification. `_Thrd_id` returns `scePthreadSelf()` unchanged, as measured on a PS5 (firmware 13.40) in two threads (#4164, #8711); `_Launch` omits `_Cnd_do_broadcast_at_thread_exit` and calls `pthread_create` registered by libkernel with default attributes. The 32-byte pad layout (`_Go()` vtable at 0x0, `_Cnd_t` at 0x8, `_Mtx_t` at 0x10, `bool _Started` at 0x18) is assumed from VC12, unverified. Failing primitives or thread start throw the guest `std::system_error`; null arguments throw. `_Thrd_join` returns `_Thrd_error` (4) on `pthread_join` failure and truncates the exit code to `int`; `_Thrd_detach` returns `_Thrd_error` (4) on `pthread_detach` failure and throws when none is registered - [siginterrupt](../../../core/libs/prx/libc/src/Process.cpp) (libc) runs FreeBSD 11.4's `siginterrupt.c` sequence through the `sigaction` and the `_sigintr` set that libkernel registers at load: it reads the signal's disposition, sets or clears the signal's bit and `SA_RESTART`, and installs the disposition again. The set is written outside libkernel's registration lock, as FreeBSD's libc writes it without one; a call before libkernel has registered throws. - [collate vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, NID `ruZtIwbCFjk`) is nine zeroed `uintptr_t` slots matching `st_size` 72 on the title `libc.prx` from PPSA16833 (offset-to-top, typeinfo, two destructors, `_Incref`/`_Decref`, and three `do_*` virtuals). Facet calls through the object fault until those methods are wired; `_Getcat` still installs the host `g_collateVtable` for facets it builds. - [num_get::id, time_get::id and the num_get vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) are data objects, as the public SDK's libSceLibcInternal table declares them and as the other facet ids and the `num_put` and `codecvt` vtables are kept: the ids start at zero and the vtable is 17 zeroed slots, the 136-byte size of `KfcTPbeaOqg` in the console's libSceLibcInternal, whose relocations fill the offset-to-top and typeinfo entries, the two destructors, `locale::facet::_Incref` and `_Decref`, and the eleven `do_get` of Dinkumware's crt.vc12 `xlocnum`, so a guest resolves the symbols and reads data, and a facet call through the vtable faults. libSceAmpr exported the three as throwing functions, which a data consumer reads as code; its qualified imports fall back to libc. From 4ac69b8d08bb2c314dd15acc2ce26458d8cacce7 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sun, 25 Oct 2026 12:55:11 +1300 Subject: [PATCH 305/428] test(libc): use the public headers instead of local declarations --- core/libs/tests/GuestStringCompare.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/core/libs/tests/GuestStringCompare.cpp b/core/libs/tests/GuestStringCompare.cpp index a30c08e959..9fc7691c39 100644 --- a/core/libs/tests/GuestStringCompare.cpp +++ b/core/libs/tests/GuestStringCompare.cpp @@ -1,13 +1,8 @@ +#include "prx/libc/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include -extern "C" { -int APS5_VABI strcmp_nid_postfix(const char*, const char*); -int APS5_VABI strncmp_nid_postfix(const char*, const char*, std::size_t); -int APS5_VABI memcmp_nid_postfix(const void*, const void*, std::size_t); -} - namespace { void Require(bool value) { From dacc812a25611ee5793d75b1f6bab247863524d0 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sun, 25 Oct 2026 12:57:37 +1300 Subject: [PATCH 306/428] test(libSceAgcDriver): use the public headers instead of local declarations --- .../tests/execution/LinearTargetCompletionPoints.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCompletionPoints.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCompletionPoints.cpp index 28c1d9aecc..8c8bd50ef9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCompletionPoints.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetCompletionPoints.cpp @@ -1,3 +1,4 @@ +#include "prx/libkernel/include/Api.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" @@ -39,8 +40,6 @@ #include #include -extern "C" int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name); -extern "C" int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq); namespace AgcDriver { From 3dd2e8a8ce2ea0fe09f0ddb8c9d5d6d98aff4b1a Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sun, 25 Oct 2026 12:58:23 +1300 Subject: [PATCH 307/428] refactor(libScePosixForWebKit): declare pthread_setname_np in the public header --- core/libs/prx/libScePosixForWebKit/include/Api.hpp | 1 + core/libs/tests/GuestPosixThreadName.cpp | 8 ++------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libScePosixForWebKit/include/Api.hpp b/core/libs/prx/libScePosixForWebKit/include/Api.hpp index fdd68aa8f7..bff7385004 100644 --- a/core/libs/prx/libScePosixForWebKit/include/Api.hpp +++ b/core/libs/prx/libScePosixForWebKit/include/Api.hpp @@ -23,6 +23,7 @@ int APS5_VABI __wrap_symlink(); int APS5_VABI clock_gettime_np(); int APS5_VABI getenv_np(); int APS5_VABI scePosixForWebKit_gethostbyname_r(); +int APS5_VABI pthread_setname_np_nid_postfix(Pthread thread, const char* name); int APS5_VABI mmap_np(); int APS5_VABI msetname(); int APS5_VABI pthread_setschedparam_np(); diff --git a/core/libs/tests/GuestPosixThreadName.cpp b/core/libs/tests/GuestPosixThreadName.cpp index 51c0691368..05f284f0db 100644 --- a/core/libs/tests/GuestPosixThreadName.cpp +++ b/core/libs/tests/GuestPosixThreadName.cpp @@ -1,15 +1,11 @@ #include "SceTypes.hpp" +#include "prx/libScePosixForWebKit/include/Api.hpp" +#include "prx/libkernel/include/Api.hpp" #include #include #include #include -extern "C" { -Pthread APS5_VABI pthread_self_nid_postfix(void); -int APS5_VABI pthread_getname_np_nid_postfix(Pthread thread, char* name); -int APS5_VABI pthread_setname_np_nid_postfix(Pthread thread, const char* name); -} - static constexpr int GUEST_EINVAL = 22; static constexpr int GUEST_ENAMETOOLONG = 63; From 72583a0b20ceb65ebaf630775ce9c0e42137a625 Mon Sep 17 00:00:00 2001 From: Disaster Date: Fri, 23 Oct 2026 05:29:55 +0300 Subject: [PATCH 308/428] feat(libc): implement _Divide, _FDivide, _Recip and _FRecip --- core/libs/prx/libc/include/Api.hpp | 4 +++ .../libs/prx/libc/src/DinkumwareInternals.cpp | 16 ++++++++++++ core/libs/tests/GuestDinkumwareInternals.cpp | 25 +++++++++++++++++++ docs/dev/debt/libc.md | 1 + 4 files changed, 46 insertions(+) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index afdb17d63c..7d35ea941e 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -610,6 +610,10 @@ float APS5_VABI roundf_nid_postfix(float x); double APS5_VABI round_nid_postfix(double x); float APS5_VABI cbrtf_nid_postfix(float x); float APS5_VABI remainderf_nid_postfix(float x, float y); +double APS5_VABI _Divide_nid_postfix(double x, double y); +float APS5_VABI _FDivide_nid_postfix(float x, float y); +double APS5_VABI _Recip_nid_postfix(double x); +float APS5_VABI _FRecip_nid_postfix(float x); float APS5_VABI nextafterf_nid_postfix(float x, float y); double APS5_VABI nextafter_nid_postfix(double x, double y); int APS5_VABI __isfinite_nid_postfix(double x); diff --git a/core/libs/prx/libc/src/DinkumwareInternals.cpp b/core/libs/prx/libc/src/DinkumwareInternals.cpp index d1f8ac33f4..8ef3fd8bdb 100644 --- a/core/libs/prx/libc/src/DinkumwareInternals.cpp +++ b/core/libs/prx/libc/src/DinkumwareInternals.cpp @@ -143,6 +143,22 @@ long double APS5_VABI _LSin_nid_postfix(long double x, unsigned int quadrant) { return quadrant != 0 ? cosl_nid_postfix(x) : sinl_nid_postfix(x); } +double APS5_VABI _Divide_nid_postfix(double x, double y) { + return x / y; +} + +float APS5_VABI _FDivide_nid_postfix(float x, float y) { + return x / y; +} + +double APS5_VABI _Recip_nid_postfix(double x) { + return 1.0 / x; +} + +float APS5_VABI _FRecip_nid_postfix(float x) { + return 1.0f / x; +} + double APS5_VABI _Log_nid_postfix(double x, int base) { if (base > 0) return log10_nid_postfix(x); return base == 0 ? log_nid_postfix(x) : log2_nid_postfix(x); diff --git a/core/libs/tests/GuestDinkumwareInternals.cpp b/core/libs/tests/GuestDinkumwareInternals.cpp index 6e1ffdf7e4..27c70368fa 100644 --- a/core/libs/tests/GuestDinkumwareInternals.cpp +++ b/core/libs/tests/GuestDinkumwareInternals.cpp @@ -63,6 +63,30 @@ void TestStoll() { Require(_Stoll_nid_postfix("9223372036854775808", &end, 10) == 9223372036854775807ll && *__error_nid_postfix() == RangeError, "_Stoll saturates"); } +void TestArithmetic() { + Require(_Divide_nid_postfix(3.0, 2.0) == 1.5, "_Divide quotient"); + Require(std::isinf(_Divide_nid_postfix(1.0, 0.0)) && !std::signbit(_Divide_nid_postfix(1.0, 0.0)), "_Divide positive infinity"); + Require(std::isinf(_Divide_nid_postfix(-1.0, 0.0)) && std::signbit(_Divide_nid_postfix(-1.0, 0.0)), "_Divide negative infinity"); + Require(std::isnan(_Divide_nid_postfix(0.0, 0.0)), "_Divide invalid"); + Require(_Divide_nid_postfix(-0.0, 2.0) == 0.0 && std::signbit(_Divide_nid_postfix(-0.0, 2.0)), "_Divide signed zero"); + Require(_Divide_nid_postfix(1.0, INFINITY) == 0.0 && !std::signbit(_Divide_nid_postfix(1.0, INFINITY)), "_Divide finite by infinity"); + Require(std::isnan(_Divide_nid_postfix(INFINITY, INFINITY)), "_Divide infinity by infinity"); + Require(_Recip_nid_postfix(4.0) == 0.25, "_Recip quotient"); + Require(_Recip_nid_postfix(-4.0) == -0.25, "_Recip negative quotient"); + Require(std::isinf(_Recip_nid_postfix(-0.0)) && std::signbit(_Recip_nid_postfix(-0.0)), "_Recip signed infinity"); + Require(_Recip_nid_postfix(INFINITY) == 0.0 && !std::signbit(_Recip_nid_postfix(INFINITY)), "_Recip infinity"); + Require(std::isnan(_Recip_nid_postfix(NAN)), "_Recip NaN"); + Require(_FDivide_nid_postfix(3.0f, 2.0f) == 1.5f, "_FDivide quotient"); + Require(std::isinf(_FDivide_nid_postfix(-1.0f, 0.0f)) && std::signbit(_FDivide_nid_postfix(-1.0f, 0.0f)), "_FDivide negative infinity"); + Require(std::isnan(_FDivide_nid_postfix(0.0f, 0.0f)), "_FDivide invalid"); + Require(_FDivide_nid_postfix(-0.0f, 2.0f) == 0.0f && std::signbit(_FDivide_nid_postfix(-0.0f, 2.0f)), "_FDivide signed zero"); + Require(_FRecip_nid_postfix(4.0f) == 0.25f, "_FRecip quotient"); + Require(_FRecip_nid_postfix(-4.0f) == -0.25f, "_FRecip negative quotient"); + Require(std::isinf(_FRecip_nid_postfix(-0.0f)) && std::signbit(_FRecip_nid_postfix(-0.0f)), "_FRecip signed infinity"); + Require(_FRecip_nid_postfix(INFINITY) == 0.0f && !std::signbit(_FRecip_nid_postfix(INFINITY)), "_FRecip infinity"); + Require(std::isnan(_FRecip_nid_postfix(NAN)), "_FRecip NaN"); +} + void TestFloatErrors() { char* end = nullptr; int error = 99; @@ -173,5 +197,6 @@ int main() { TestStoll(); TestFloatErrors(); TestIntegerErrors(); + TestArithmetic(); TestDispatch(); } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index bd27bf7a89..ce07580056 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -69,6 +69,7 @@ - [std::thread::hardware_concurrency](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `CHrhwd8QSBs`) returns the host's `std::thread::hardware_concurrency()` (which may be 0 when unknown) instead of the console's processor count; no reference implements it and the value a title expects (PPSA34015 imports it) is unverified. - [fmodf](../../../core/libs/prx/libc/src/Math.cpp) (libc) - console `errno`, exception flags and NaN payloads remain unverified. The wrapper preserves `errno` like the existing double `fmod`, following FreeBSD 11.4 [`e_fmodf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_fmodf.c); host `std::fmod` still supplies the result and exception flags. - [_FSinh and _FCosh](../../../core/libs/prx/libc/src/Math.cpp) (libc) finite results are not verified bit for bit against the console: the scaled product uses the host's binary64 hyperbolic function before rounding to binary32, rather than the float rational and `_FExp` sequence in PRAGMATA's bundled `libc.prx`. NaN preference and payloads, signed zeros, infinite operands and the range-error thresholds follow that binary's instructions; the system `libSceLibcInternal` implementation and nondefault flush-to-zero modes are unverified. Against `libSceLibcInternal.sprx` on a PS5 (firmware 13.60), 12 of 36 measured results differ by one unit in the last place (`_FSinh(0.5, y)` and `_FCosh(1, y)`, `_FCosh(10, y)` for y of 0.5, 1 and 2); zeros, the overflow to infinity at 89 and 90 and the tiny-argument results matched +- [_Divide, _FDivide, _Recip and _FRecip](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libc) follow the Dinkumware CRT division and reciprocal contracts with IEEE-754 signed-zero, infinity and NaN behavior. The exact console results, signaling-NaN handling and floating-point exception behavior are not verified. - [sscanf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) return `EOF` when a reached conversion has a null destination, stopping before further assignments, following C11 K.3.5.3.7 and K.3.5.3.14. Earlier assignments are retained. The constraint handler is not invoked, and console behaviour is unverified. - [memmem and memccpy](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `memmem` searches for needle occurrences in memory buffers and `memccpy` copies buffers until a character match, following FreeBSD and POSIX specifications; behavior is unverified on console. From b3c9d35ae250ba853803965dd522312c6cf9fdfa Mon Sep 17 00:00:00 2001 From: Eduardo Ruiz Date: Sat, 24 Oct 2026 19:32:27 +0200 Subject: [PATCH 309/428] feat(libc): implement _Mtx_timedlock --- core/libs/prx/libc/include/Api.hpp | 1 + core/libs/prx/libc/src/Threads.cpp | 121 ++++++++++----------------- core/libs/tests/GuestLibcThreads.cpp | 41 +++++++++ docs/dev/debt/libc.md | 1 + 4 files changed, 87 insertions(+), 77 deletions(-) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index afdb17d63c..735aa060b3 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -987,6 +987,7 @@ int APS5_VABI _Mtx_init_with_name_nid_postfix(void** handle, int type, const cha void APS5_VABI _Mtx_destroy_nid_postfix(void** handle); int APS5_VABI _Mtx_lock_nid_postfix(void** handle); int APS5_VABI _Mtx_trylock_nid_postfix(void** handle); +int APS5_VABI _Mtx_timedlock_nid_postfix(void** handle, const Xtime* target); int APS5_VABI _Mtx_unlock_nid_postfix(void** handle); int APS5_VABI _Cnd_init_nid_postfix(void** handle); int APS5_VABI _Cnd_init_with_default_name_override_nid_postfix(void** handle, const char*); diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index 8f13974d7a..8e9566b718 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -48,39 +48,35 @@ timespec XtimeDeadline(const Xtime& time, const char* function) { return {static_cast(seconds), static_cast(nanos)}; } +std::chrono::system_clock::time_point XtimePoint(const Xtime& time, const char* function) { + const auto deadline = XtimeDeadline(time, function); + constexpr auto maxDuration = std::chrono::system_clock::duration::max(); + constexpr auto minDuration = std::chrono::system_clock::duration::min(); + constexpr auto maxSeconds = std::chrono::duration_cast(maxDuration).count(); + constexpr auto minSeconds = std::chrono::duration_cast(minDuration).count(); + constexpr auto maxNanos = std::chrono::duration_cast(maxDuration - std::chrono::seconds(maxSeconds)).count(); + if (deadline.tv_sec > maxSeconds || (deadline.tv_sec == maxSeconds && deadline.tv_nsec > maxNanos) || + deadline.tv_sec < minSeconds) + throw std::overflow_error(std::string(function) + ": xtime seconds overflow"); + return std::chrono::system_clock::time_point( + std::chrono::seconds(deadline.tv_sec) + + std::chrono::ceil(std::chrono::nanoseconds(deadline.tv_nsec))); +} + constexpr int threadSuccess = 0; constexpr int threadNomem = 1; constexpr int threadTimedout = 2; constexpr int threadBusy = 3; constexpr int threadError = 4; +constexpr int threadTimedlockError = 5; constexpr int mutexRecursive = 0x100; constexpr std::uint64_t mutexTag = 0x5854'4D5F'4D54'5844ull; constexpr std::uint64_t conditionTag = 0x5854'4E43'5F44'4E43ull; -#ifdef _WIN32 -struct DinkumwareNativeMutex { - CRITICAL_SECTION value; - - DinkumwareNativeMutex() { - if (!InitializeCriticalSectionAndSpinCount(&value, 0)) - throw std::system_error(static_cast(GetLastError()), std::system_category(), "_Mtx_init: initializing critical section failed"); - } - - ~DinkumwareNativeMutex() { DeleteCriticalSection(&value); } - void lock() { EnterCriticalSection(&value); } - void unlock() { LeaveCriticalSection(&value); } - bool try_lock() { return TryEnterCriticalSection(&value) != FALSE; } -}; - -struct DinkumwareNativeCondition { - CONDITION_VARIABLE value = CONDITION_VARIABLE_INIT; -}; -#else -using DinkumwareNativeMutex = std::mutex; -using DinkumwareNativeCondition = std::condition_variable; -#endif +using DinkumwareNativeMutex = std::timed_mutex; +using DinkumwareNativeCondition = std::condition_variable_any; struct DinkumwareMutex { std::uint64_t tag = mutexTag; @@ -95,32 +91,6 @@ struct DinkumwareCondition { DinkumwareNativeCondition native; }; -#ifdef _WIN32 -DWORD WaitMillisecondsUntil(std::chrono::system_clock::time_point deadline) { - const auto now = std::chrono::system_clock::now(); - if (now >= deadline) - return 0; - const auto remaining = std::chrono::ceil(deadline - now).count(); - constexpr auto largest = static_cast(std::numeric_limits::max() - 1U); - return static_cast(std::min(remaining, largest)); -} - -bool WaitUntil(DinkumwareCondition* condition, DinkumwareMutex* mutex, std::chrono::system_clock::time_point deadline) { - for (;;) { - const DWORD milliseconds = WaitMillisecondsUntil(deadline); - if (milliseconds == 0) - return false; - if (SleepConditionVariableCS(&condition->native.value, &mutex->native.value, milliseconds) != FALSE) - return true; - const DWORD error = GetLastError(); - if (error != ERROR_TIMEOUT) - throw std::system_error(static_cast(error), std::system_category(), "_Cnd_timedwait: condition wait failed"); - if (std::chrono::system_clock::now() >= deadline) - return false; - } -} -#endif - DinkumwareMutex* ResolveMutex(void** handle, const char* function) { if (!handle) throw std::invalid_argument(std::string(function) + ": null mutex handle pointer"); @@ -219,6 +189,31 @@ int APS5_VABI _Mtx_trylock_nid_postfix(void** handle) { return threadSuccess; } +int APS5_VABI _Mtx_timedlock_nid_postfix(void** handle, const Xtime* target) { + if (!target) + APS5_INVALID_ARG_EX; + const auto deadline = XtimePoint(*target, "_Mtx_timedlock"); + auto* mutex = ResolveMutex(handle, "_Mtx_timedlock"); + const auto self = std::this_thread::get_id(); + if (mutex->owner.load(std::memory_order_acquire) == self) { + if ((mutex->type & mutexRecursive) == 0) + return threadBusy; + if (mutex->count == std::numeric_limits::max()) + throw std::overflow_error("_Mtx_timedlock: recursive lock count overflow"); + ++mutex->count; + return threadSuccess; + } + try { + if (!mutex->native.try_lock_until(deadline)) + return threadTimedout; + } catch (const std::system_error&) { + return threadTimedlockError; + } + mutex->owner.store(self, std::memory_order_release); + mutex->count = 1; + return threadSuccess; +} + int APS5_VABI _Mtx_unlock_nid_postfix(void** handle) { auto* mutex = ResolveMutex(handle, "_Mtx_unlock"); if (!IsOwnedByCaller(mutex)) @@ -267,12 +262,7 @@ int APS5_VABI _Cnd_wait_nid_postfix(void** conditionHandle, void** mutexHandle) mutex->owner.store(std::thread::id{}, std::memory_order_release); std::unique_lock lock(mutex->native, std::adopt_lock); try { -#ifdef _WIN32 - if (SleepConditionVariableCS(&condition->native.value, &mutex->native.value, INFINITE) == FALSE) - throw std::system_error(static_cast(GetLastError()), std::system_category(), "_Cnd_wait: condition wait failed"); -#else condition->native.wait(lock); -#endif } catch (const std::system_error&) { lock.release(); mutex->owner.store(std::this_thread::get_id(), std::memory_order_release); @@ -288,15 +278,7 @@ int APS5_VABI _Cnd_wait_nid_postfix(void** conditionHandle, void** mutexHandle) int APS5_VABI _Cnd_timedwait_nid_postfix(void** conditionHandle, void** mutexHandle, const Xtime* target) { if (!target) APS5_INVALID_ARG_EX; - const auto deadline = XtimeDeadline(*target, "_Cnd_timedwait"); - constexpr auto maxDuration = std::chrono::system_clock::duration::max(); - constexpr auto minDuration = std::chrono::system_clock::duration::min(); - constexpr auto maxSeconds = std::chrono::duration_cast(maxDuration).count(); - constexpr auto minSeconds = std::chrono::duration_cast(minDuration).count(); - constexpr auto maxNanos = std::chrono::duration_cast(maxDuration - std::chrono::seconds(maxSeconds)).count(); - if (deadline.tv_sec > maxSeconds || (deadline.tv_sec == maxSeconds && deadline.tv_nsec > maxNanos) || - deadline.tv_sec < minSeconds) - throw std::overflow_error("_Cnd_timedwait: xtime seconds overflow"); + const auto deadlineTime = XtimePoint(*target, "_Cnd_timedwait"); auto* condition = ResolveCondition(conditionHandle, "_Cnd_timedwait"); auto* mutex = ResolveMutex(mutexHandle, "_Cnd_timedwait"); if (!IsOwnedByCaller(mutex)) @@ -308,14 +290,7 @@ int APS5_VABI _Cnd_timedwait_nid_postfix(void** conditionHandle, void** mutexHan std::unique_lock lock(mutex->native, std::adopt_lock); bool timeout = false; try { - const auto deadlineTime = std::chrono::system_clock::time_point( - std::chrono::seconds(deadline.tv_sec) + - std::chrono::ceil(std::chrono::nanoseconds(deadline.tv_nsec))); -#ifdef _WIN32 - timeout = !WaitUntil(condition, mutex, deadlineTime); -#else timeout = (condition->native.wait_until(lock, deadlineTime) == std::cv_status::timeout); -#endif } catch (const std::system_error&) { lock.release(); mutex->owner.store(std::this_thread::get_id(), std::memory_order_release); @@ -329,20 +304,12 @@ int APS5_VABI _Cnd_timedwait_nid_postfix(void** conditionHandle, void** mutexHan } int APS5_VABI _Cnd_broadcast_nid_postfix(void** handle) { -#ifdef _WIN32 - WakeAllConditionVariable(&ResolveCondition(handle, "_Cnd_broadcast")->native.value); -#else ResolveCondition(handle, "_Cnd_broadcast")->native.notify_all(); -#endif return threadSuccess; } int SignalCondition_nid_no_patch(void** handle) { -#ifdef _WIN32 - WakeConditionVariable(&ResolveCondition(handle, "_Cnd_signal")->native.value); -#else ResolveCondition(handle, "_Cnd_signal")->native.notify_one(); -#endif return threadSuccess; } diff --git a/core/libs/tests/GuestLibcThreads.cpp b/core/libs/tests/GuestLibcThreads.cpp index 1d2b3d7dff..7d1d82ed3b 100644 --- a/core/libs/tests/GuestLibcThreads.cpp +++ b/core/libs/tests/GuestLibcThreads.cpp @@ -28,6 +28,7 @@ static void RequireThrows(TCall call) { } constexpr int success = 0; +constexpr int timedout = 2; constexpr int busy = 3; constexpr int plain = 0x01; constexpr int tryable = 0x02; @@ -40,6 +41,14 @@ static void* CreateMutex(int type) { return mutex; } +static Xtime RelativeTime(int milliseconds) { + const auto point = std::chrono::system_clock::now() + std::chrono::milliseconds(milliseconds); + const auto since = point.time_since_epoch(); + const auto seconds = std::chrono::duration_cast(since); + const auto nanos = std::chrono::duration_cast(since - seconds); + return {seconds.count(), nanos.count()}; +} + static void MutexBasics() { void* mutex = CreateMutex(tryable); Require(_Mtx_lock_nid_postfix(&mutex) == success); @@ -110,6 +119,37 @@ static void RecursiveMutex() { } +static void TimedMutex() { + void* mutex = CreateMutex(tryable); + Xtime future = RelativeTime(60000); + Require(_Mtx_timedlock_nid_postfix(&mutex, &future) == success); + Require(_Mtx_timedlock_nid_postfix(&mutex, &future) == busy); + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + + Require(_Mtx_lock_nid_postfix(&mutex) == success); + int result = -1; + std::thread worker([&] { + Xtime deadline = RelativeTime(30); + result = _Mtx_timedlock_nid_postfix(&mutex, &deadline); + }); + worker.join(); + Require(result == timedout); + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + + void* empty = nullptr; + RequireThrows([&] { _Mtx_timedlock_nid_postfix(&empty, &future); }); + RequireThrows([&] { _Mtx_timedlock_nid_postfix(nullptr, &future); }); + RequireThrows([&] { _Mtx_timedlock_nid_postfix(&mutex, nullptr); }); + _Mtx_destroy_nid_postfix(&mutex); + + void* recursiveMutex = CreateMutex(recursive); + Require(_Mtx_timedlock_nid_postfix(&recursiveMutex, &future) == success); + Require(_Mtx_timedlock_nid_postfix(&recursiveMutex, &future) == success); + Require(_Mtx_unlock_nid_postfix(&recursiveMutex) == success); + Require(_Mtx_unlock_nid_postfix(&recursiveMutex) == success); + _Mtx_destroy_nid_postfix(&recursiveMutex); +} + static void MutualExclusion() { void* mutex = CreateMutex(tryable); long counter = 0; @@ -440,6 +480,7 @@ int main() { ForwardedCalls(); MutexBasics(); RecursiveMutex(); + TimedMutex(); MutualExclusion(); ConditionBroadcast(); ConditionSignal(); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 2d4e0fc01d..71db1cce83 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -37,6 +37,7 @@ - [__atomic_load, __atomic_store, __atomic_exchange and __atomic_compare_exchange](../../../core/libs/prx/libc/src/AtomicBuiltins.cpp) (libc) - the generic, size-parameterised libatomic entry points are a single `ud2` in the `libc.prx` bundled with PPSA08427 (disassembly), so the console traps on any call; these throw instead - [Sized __atomic calls](../../../core/libs/prx/libc/src/DinkumwareAtomics.cpp) (libc) - the 36 `__atomic_load_N`, `__atomic_store_N`, `__atomic_exchange_N`, `__atomic_compare_exchange_N` and `__atomic_fetch_{add,sub,and,or,xor}_N` calls (N = 1, 2, 4, 8) are sequentially consistent whatever order they are given. The `libc.prx` bundled with PPSA08427, run side by side on random values and on every order from -1 to 7, switches on the order but always uses a locked instruction or CAS loop, except that a store with an order other than `memory_order_seq_cst` is a plain store, so these are never weaker than the console. Unlike `_Atomic_load_8` and the 8-byte `_Atomic_*` exchange and fetch calls above, the console accepts every order here, including release, acq_rel and invalid ones, and these do the same - [Dinkumware mutexes and conditions](../../../core/libs/prx/libc/src/Threads.cpp) (libc) use pointer-to-handle arguments, matching the console aerolib signatures reported in [#3079](https://github.com/boykopovar/AnyPS5/pull/3079). The title's `_Mtx_lock` maps `0` to `_Thrd_success` (0), `0x8002000b` to `_Thrd_busy` (3), and other failures to `_Thrd_error` (4); its internal mutex init recognizes recursive flag `0x100`, while every other type uses default attributes. The observed relock failure for a plain mutex suggests those defaults are error-checking, but that has not been confirmed on hardware. The title's `_Mtx_unlock` and `_Cnd_wait` return success regardless of their internal result. The host implementation treats invalid ownership preconditions as no-ops, and throws for invalid handles, host wait errors and destroying a locked mutex as debug guards; those cases are not confirmed against the console. +- [_Mtx_timedlock](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `hPzYSd5Nasc`) follows the console's `libc.prx` at `0x5d50` (PPSA02530): it takes an absolute xtime and returns `_Thrd_timedout` (2) on expiry (`0x8002003c`) or 5 on other failures. The native mutex and condition are `std::timed_mutex` and `std::condition_variable_any`, replacing the Windows `CRITICAL_SECTION`/`CONDITION_VARIABLE` pair; Windows timing is not measured on the console. - [Floating-point compiler-rt builtins](../../../core/libs/prx/libc/src/FloatBuiltins.cpp) (libc) follow the `libc.prx` bundled with PPSA08427 (disassembly, and that binary run side by side on random inputs) where it differs from current compiler-rt: the soft-float `__add`, `__sub`, `__mul` and `__div` (`sf3`, `df3`) round to nearest, raise no exception flags and ignore MXCSR, here done with SSE under a default MXCSR that is restored afterwards; division flushes a quotient below the smallest normal to a signed zero; `__extendsfdf2` and `__truncdfsf2` copy NaN payloads unshifted; float-to-integer conversions do not saturate but shift the significand by a count masked to the shift width. `__mulsc3`, `__muldc3`, `__divsc3` and `__divdc3` fuse the same products as the console with `fma` and scale through the x87 like its `scalbn`, so their values match, but NaN payloads and exception flags can differ, as the host `fma` and `logb` are not the console's VEX FMA code and libm - [random, srandom, initstate, setstate](../../../core/libs/prx/libc/src/Random.cpp) (libc) - not measured: the state `setstate` returns and its null return for a rear position past the degree, `errno` after `initstate` below 8 bytes, a null state (throws here), an `initstate` seed other than 1, concurrent callers (one lock serialises every call here) and a title's own libc. Measured in a payload process on a PS5 (firmware 12.70): a fresh process draws the `srandom(1)` sequence; `srandom` seeds an additive feedback generator through a `good_rand` that reads its argument as an int32, turns 0 into 123459876 and applies `x = 16807 * (x % 127773) - 2836 * (x / 127773)`, adding 0x7fffffff to a negative result, which reproduces seeds 0, 1, 2, 12345, 0xffffffff and 0x80000000; `initstate` returns null at 0 and 7 bytes and selects types 0 to 4 by size as measured at 8, 15, 31, 32, 63, 64, 127, 128, 129, 255, 256, 257 and 1000 bytes. - [srandomdev](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) is not verified against the console. The firmware 3.20 [export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibcInternal.c) names NID `a2MOZf++Wjg`; it follows FreeBSD 11 [random.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdlib/random.c), filling every active generator word from host OS entropy instead of `KERN_ARND`, then resetting additive feedback positions without warming up. An entropy failure throws rather than leaving `sysctl`'s failure unchecked. Calls share the generator lock. From f23ef996998ab06bcc0d74ef5a2e8f4588b2cb4b Mon Sep 17 00:00:00 2001 From: slopgobblercobbler Date: Sun, 25 Oct 2026 02:36:47 +0100 Subject: [PATCH 310/428] feat(libSceAgcDriver): cover payload packing in sceAgcDriverNopDataWritePacket --- .../libSceAgcDriver/tests/NopDataWritePacket.cpp | 13 +++++++++++++ docs/dev/debt/libSceAgc.md | 1 + docs/dev/debt/libSceAgcDriver.md | 3 ++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/NopDataWritePacket.cpp b/core/libs/prx/libSceAgcDriver/tests/NopDataWritePacket.cpp index d3e4092bd2..6be7c6011a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/NopDataWritePacket.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/NopDataWritePacket.cpp @@ -33,5 +33,18 @@ int main() { std::memcpy(&address, storage.data() + 8, sizeof(address)); if (zeroBytesWords != 2 || header != 0xc0001000U || address != 0x01020304U) return 7; if (storage[12] != std::byte{0xaa}) return 8; + + for (std::uint32_t len = 1; len <= 4; ++len) { + storage.fill(std::byte{0xaa}); + const auto lenWords = sceAgcDriverNopDataWritePacket(storage.data() + 4, 0xaabbccddU, data.data(), len); + std::memcpy(&header, storage.data() + 4, sizeof(header)); + std::memcpy(&address, storage.data() + 8, sizeof(address)); + if (lenWords != 3 || header != 0xc0011000U || address != 0xaabbccddU) return 9 + static_cast(len); + if (!std::equal(data.begin(), data.begin() + len, storage.begin() + 12)) return 14 + static_cast(len); + for (std::size_t pad = 12 + len; pad < 16; ++pad) { + if (storage[pad] != std::byte{0}) return 19 + static_cast(len); + } + if (storage[3] != std::byte{0xaa} || storage[16] != std::byte{0xaa}) return 24 + static_cast(len); + } return 0; } diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index aa30dca0b0..c10c92eab4 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -48,3 +48,4 @@ - [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) (libSceAgc) - non-sample-dump event types (any event other than 0x38 and 0x39) emit a 2-dword PM4 packet 0x46 without address dwords; the address argument is ignored for these events. Void Terrarium (PPSA03061, #5055) passes a non-null address buffer (0x00000002d2a10000) with event type 0x16 (cache flush). - This pending declaration has an unverified signature and behavior; it is a parameterless import-resolution declaration that throws through `NotImplemented_nid_no_patch`: [`sceAgcAcbInitializeDefaultHardwareState`](../../../core/libs/prx/libSceAgc/Unimplemented.cpp). +- [sceAgcAcquireMemSetEngine, sceAgcCbMemsetExclusive](../../../core/libs/prx/libSceAgc/Unimplemented.cpp) (libSceAgc) - declared with canonical signatures from decrypted headers; behavior on retail consoles remains unmeasured. Both throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 0f69cda538..608bedf97c 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -11,7 +11,8 @@ - [sceAgcDriverSubmitAcb, sceAgcDriverSubmitMultiAcbs](../../../core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp) (libSceAgcDriver) accept compute queues 0x20 to 0x57 and throw for any other. On a PS5 (firmware 13.40, measured 2026-10-19 from inside a retail title) `sceAgcDriverSubmitAcb` returned 0 and ran a label-only buffer on queues 0x20, 0x30, 0x40, 0x50, 0x57 and also 0x58, which throws here; queue 0x1F faulted inside the console's driver (SIGSEGV in libkernel on the calling thread), which throws here too. Queues above 0x58 and `sceAgcDriverSubmitMultiAcbs` were not measured. Closes when: the console's highest accepted compute queue is measured -- [sceAgcDriverAcquireHDRScopesACQ, sceAgcDriverHp3dLockEnd, sceAgcDriverHp3dLockStart, sceAgcDriverIsEsvmProcess, sceAgcDriverNopDataWritePacket, sceAgcDriverReleaseHDRScopesACQ, sceAgcDriverSetValueToSharedGds, sceAgcDriverSetWorkloadStreamInactive, sceAgcDriverSubmitReprojectionAcb, sceAgcDriverSuspendPointSubmitCdbg, sceAgcDriverSysGetGfxAppGpuResetStatus](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - signatures and behavior are unverified; these parameterless declarations only resolve imports and throw through `NotImplemented_nid_no_patch`. +- [sceAgcDriverAcquireHDRScopesACQ, sceAgcDriverHp3dLockEnd, sceAgcDriverHp3dLockStart, sceAgcDriverIsEsvmProcess, sceAgcDriverReleaseHDRScopesACQ, sceAgcDriverSetValueToSharedGds, sceAgcDriverSetWorkloadStreamInactive, sceAgcDriverSubmitReprojectionAcb, sceAgcDriverSuspendPointSubmitCdbg, sceAgcDriverSysGetGfxAppGpuResetStatus](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - signatures and behavior are unverified; these parameterless declarations only resolve imports and throw through `NotImplemented_nid_no_patch`. +- [sceAgcDriverNopDataWritePacket](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - writes an IT_NOP packet with 32-bit address at dword 1 and payload at dword 2, returning 2 dwords for null data or zero bytes, and total dwords otherwise. Console behavior for null or unaligned destination remains unmeasured. - [sceAgcDriverGetCoredumpAddress](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) returns the driver's shared coredump-area pointer, which is null when no coredump area is allocated. AnyPS5 reports coredump mode disabled and does not allocate this area; the export returns null. Enabling and populating an AGC coredump area is not implemented. - [sceAgcDriverSetTargetRingForDiag](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) returns `0x8A6D0001` (`DRIVER_FEATURE_UNAVAILABLE`), matching the retail PS5 export. The retail diagnostic log message is not reproduced. From 60a0f0f5e2ef9ee462718108dd3a27a8a1c495bd Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:45:04 +0200 Subject: [PATCH 311/428] fix(libSceFreeType): preserve referenced face lifetimes --- core/libs/prx/libSceFreeType/Export.cpp | 8 +- core/libs/prx/libSceFreeType/include/Api.hpp | 1 + .../libSceFreeType/include/GuestObjects.hpp | 1 + core/libs/tests/GuestFreeType.cpp | 126 +++++++++++++++++- docs/dev/debt/libSceFreeType.md | 1 + 5 files changed, 133 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceFreeType/Export.cpp b/core/libs/prx/libSceFreeType/Export.cpp index 0ff5071b79..1bf8a0759a 100644 --- a/core/libs/prx/libSceFreeType/Export.cpp +++ b/core/libs/prx/libSceFreeType/Export.cpp @@ -42,6 +42,7 @@ void FinalizeFace(void* object) { auto* host = static_cast(object); auto* face = static_cast(host->generic.data); if (face->rec.generic.finalizer) face->rec.generic.finalizer(&face->rec); + face->finalized = true; } void PublishFace(GuestLibrary* library, std::unique_ptr face, FT_Face host, GuestFaceRec** aface) { @@ -144,10 +145,15 @@ int APS5_VABI FT_New_Memory_Face_nid_postfix(GuestLibrary* library, const unsign return 0; } +int APS5_VABI FT_Reference_Face_nid_postfix(GuestFaceRec* face) { + const GuestFace* owner = FaceOf(face); + return FT_Reference_Face(owner ? owner->host : nullptr); +} + int APS5_VABI FT_Done_Face_nid_postfix(GuestFaceRec* face) { GuestFace* owner = FaceOf(face); const FT_Error error = FT_Done_Face(owner ? owner->host : nullptr); - if (error == 0) owner->library->faces.remove_if([owner](const auto& entry) { return entry.get() == owner; }); + if (error == 0 && owner->finalized) owner->library->faces.remove_if([owner](const auto& entry) { return entry.get() == owner; }); return error; } diff --git a/core/libs/prx/libSceFreeType/include/Api.hpp b/core/libs/prx/libSceFreeType/include/Api.hpp index 7c2f791458..25f82e9136 100644 --- a/core/libs/prx/libSceFreeType/include/Api.hpp +++ b/core/libs/prx/libSceFreeType/include/Api.hpp @@ -10,6 +10,7 @@ int APS5_VABI FT_Init_FreeType_nid_postfix(FreeType::GuestLibrary** alibrary); int APS5_VABI FT_Done_FreeType_nid_postfix(FreeType::GuestLibrary* library); int APS5_VABI FT_New_Face_nid_postfix(FreeType::GuestLibrary* library, const char* filepathname, std::int64_t faceIndex, FreeType::GuestFaceRec** aface); int APS5_VABI FT_New_Memory_Face_nid_postfix(FreeType::GuestLibrary* library, const unsigned char* fileBase, std::int64_t fileSize, std::int64_t faceIndex, FreeType::GuestFaceRec** aface); +int APS5_VABI FT_Reference_Face_nid_postfix(FreeType::GuestFaceRec* face); int APS5_VABI FT_Done_Face_nid_postfix(FreeType::GuestFaceRec* face); int APS5_VABI FT_Set_Pixel_Sizes_nid_postfix(FreeType::GuestFaceRec* face, std::uint32_t pixelWidth, std::uint32_t pixelHeight); int APS5_VABI FT_Set_Char_Size_nid_postfix(FreeType::GuestFaceRec* face, std::int64_t charWidth, std::int64_t charHeight, std::uint32_t horizontalResolution, std::uint32_t verticalResolution); diff --git a/core/libs/prx/libSceFreeType/include/GuestObjects.hpp b/core/libs/prx/libSceFreeType/include/GuestObjects.hpp index 9ae0c12315..081d70665f 100644 --- a/core/libs/prx/libSceFreeType/include/GuestObjects.hpp +++ b/core/libs/prx/libSceFreeType/include/GuestObjects.hpp @@ -27,6 +27,7 @@ struct GuestFace { GuestSizeRec size{}; GuestLibrary* library = nullptr; FT_Face host = nullptr; + bool finalized = false; std::vector bytes; std::vector fixedSizes; std::vector charmaps; diff --git a/core/libs/tests/GuestFreeType.cpp b/core/libs/tests/GuestFreeType.cpp index c14954f91c..1c2676dc7c 100644 --- a/core/libs/tests/GuestFreeType.cpp +++ b/core/libs/tests/GuestFreeType.cpp @@ -1,6 +1,6 @@ #include "prx/libSceFreeType/include/Api.hpp" #include "FreeTypeFontFixture.hpp" -#include "prx/libSceFreeType/include/GuestFreeType.hpp" +#include "prx/libSceFreeType/include/GuestObjects.hpp" #include "prx/libc/include/General.hpp" #include @@ -468,6 +468,7 @@ static void Trigonometry() { struct Finalizers { std::vector calls; + bool reenter = false; }; static std::vector CidMetadataFont(bool keyed, const std::string& registry, const std::string& ordering, @@ -639,7 +640,13 @@ static void NativeSizeFinalizer(void* object) { static void NativeFaceFinalizer(void* object) { auto* face = static_cast(object); Require(face->glyph == nullptr && face->size == nullptr); - static_cast(face->generic.data)->calls.push_back(3); + auto* events = static_cast(face->generic.data); + events->calls.push_back(3); + if (events->reenter) { + Require(FT_Reference_Face(face) == 0 && FT_Reference_Face(face) == 0); + Require(FT_Done_Face(face) == 0); + Require(face->glyph == nullptr && face->size == nullptr); + } } static void APS5_VABI GuestSlotFinalizer(void* object) { @@ -657,7 +664,14 @@ static void APS5_VABI GuestSizeFinalizer(void* object) { static void APS5_VABI GuestFaceFinalizer(void* object) { auto* face = static_cast(object); Require(face->glyph == nullptr && face->size == nullptr); - static_cast(face->generic.data)->calls.push_back(3); + auto* events = static_cast(face->generic.data); + events->calls.push_back(3); + if (events->reenter) { + const auto* library = static_cast(face->internal)->library; + Require(FT_Reference_Face_nid_postfix(face) == 0 && FT_Reference_Face_nid_postfix(face) == 0); + Require(FT_Done_Face_nid_postfix(face) == 0 && library->faces.size() == 1); + Require(face->glyph == nullptr && face->size == nullptr); + } } static void SetFinalizers(FT_Face face, Finalizers& events) { @@ -762,6 +776,99 @@ static void CheckTransforms(const std::vector& font) { } int main() { + { + const auto referenceFont = SquareGlyphFont('A', 700); + GuestLibrary* referenceLibrary = nullptr; + FT_Library oracle = nullptr; + Require(FT_Init_FreeType_nid_postfix(&referenceLibrary) == 0 && FT_Init_FreeType(&oracle) == 0); + auto* owners = referenceLibrary; + Require(FT_Reference_Face_nid_postfix(nullptr) == FT_Reference_Face(nullptr)); + int references = 0; + int retained = 0; + int finalized = 0; + for (const unsigned depth : {1u, 2u, 3u, 17u}) { + GuestFaceRec* shared = nullptr; + FT_Face native = nullptr; + Require(FT_New_Memory_Face_nid_postfix(referenceLibrary, referenceFont.data(), static_cast(referenceFont.size()), 0, &shared) == 0); + Require(FT_New_Memory_Face(oracle, referenceFont.data(), static_cast(referenceFont.size()), 0, &native) == 0); + Finalizers guestEvents; + Finalizers nativeEvents; + guestEvents.reenter = nativeEvents.reenter = depth == 3; + SetFinalizers(shared, guestEvents); + SetFinalizers(native, nativeEvents); + for (unsigned index = 1; index < depth; ++index) { + Require(FT_Reference_Face_nid_postfix(shared) == FT_Reference_Face(native)); + ++references; + } + for (unsigned index = 1; index < depth; ++index) { + Require(FT_Done_Face(native) == 0 && FT_Done_Face_nid_postfix(shared) == 0); + Require(owners->faces.size() == 1 && guestEvents.calls.empty() && nativeEvents.calls.empty()); + Require(FT_Get_Char_Index_nid_postfix(shared, 'A') == FT_Get_Char_Index(native, 'A')); + Require(FT_Set_Pixel_Sizes_nid_postfix(shared, 0, 16 + index) == 0 && FT_Set_Pixel_Sizes(native, 0, 16 + index) == 0); + Require(FT_Load_Char_nid_postfix(shared, 'A', FT_LOAD_RENDER) == 0 && FT_Load_Char(native, 'A', FT_LOAD_RENDER) == 0); + SlotMatches(shared->glyph, native->glyph); + ++retained; + } + Require(FT_Done_Face(native) == 0 && FT_Done_Face_nid_postfix(shared) == 0); + if (guestEvents.reenter) { + references += 2; + ++retained; + } + Require(owners->faces.empty() && guestEvents.calls == nativeEvents.calls && guestEvents.calls == std::vector({1, 2, 3})); + ++finalized; + } + GuestFaceRec* plain = nullptr; + FT_Face nativePlain = nullptr; + Require(FT_New_Memory_Face_nid_postfix(referenceLibrary, referenceFont.data(), static_cast(referenceFont.size()), 0, &plain) == 0); + Require(FT_New_Memory_Face(oracle, referenceFont.data(), static_cast(referenceFont.size()), 0, &nativePlain) == 0); + Require(FT_Reference_Face_nid_postfix(plain) == FT_Reference_Face(nativePlain)); + ++references; + Require(FT_Done_Face(nativePlain) == 0 && FT_Done_Face_nid_postfix(plain) == 0 && owners->faces.size() == 1); + ++retained; + Require(FT_Done_Face(nativePlain) == 0 && FT_Done_Face_nid_postfix(plain) == 0 && owners->faces.empty()); + ++finalized; + GuestFaceRec* probe = nullptr; + FT_Face nativeProbe = nullptr; + Require(FT_New_Memory_Face_nid_postfix(referenceLibrary, referenceFont.data(), static_cast(referenceFont.size()), -1, &probe) == 0); + Require(FT_New_Memory_Face(oracle, referenceFont.data(), static_cast(referenceFont.size()), -1, &nativeProbe) == 0); + Finalizers probeEvents; + Finalizers nativeProbeEvents; + probe->generic = {&probeEvents, GuestFaceFinalizer}; + nativeProbe->generic = {&nativeProbeEvents, NativeFaceFinalizer}; + Require(probe->glyph == nullptr && probe->size == nullptr); + Require(FT_Reference_Face_nid_postfix(probe) == FT_Reference_Face(nativeProbe)); + ++references; + Require(FT_Done_Face(nativeProbe) == 0 && FT_Done_Face_nid_postfix(probe) == 0 && owners->faces.size() == 1); + ++retained; + Require(probeEvents.calls.empty() && nativeProbeEvents.calls.empty()); + Require(FT_Done_Face(nativeProbe) == 0 && FT_Done_Face_nid_postfix(probe) == 0); + Require(owners->faces.empty() && probeEvents.calls == nativeProbeEvents.calls && probeEvents.calls == std::vector({3})); + ++finalized; + std::array shutdownEvents; + std::array nativeShutdownEvents; + std::array shutdown{}; + std::array nativeShutdown{}; + for (unsigned index = 0; index < shutdown.size(); ++index) { + Require(FT_New_Memory_Face_nid_postfix(referenceLibrary, referenceFont.data(), static_cast(referenceFont.size()), 0, &shutdown[index]) == 0); + Require(FT_New_Memory_Face(oracle, referenceFont.data(), static_cast(referenceFont.size()), 0, &nativeShutdown[index]) == 0); + SetFinalizers(shutdown[index], shutdownEvents[index]); + SetFinalizers(nativeShutdown[index], nativeShutdownEvents[index]); + for (unsigned reference = 0; reference < 3 - index; ++reference) { + Require(FT_Reference_Face_nid_postfix(shutdown[index]) == FT_Reference_Face(nativeShutdown[index])); + ++references; + } + Require(FT_Done_Face(nativeShutdown[index]) == 0 && FT_Done_Face_nid_postfix(shutdown[index]) == 0); + Require(owners->faces.size() == index + 1 && shutdownEvents[index].calls.empty()); + ++retained; + } + Require(FT_Done_FreeType(oracle) == 0 && FT_Done_FreeType_nid_postfix(referenceLibrary) == 0); + for (unsigned index = 0; index < shutdown.size(); ++index) { + Require(shutdownEvents[index].calls == nativeShutdownEvents[index].calls && shutdownEvents[index].calls == std::vector({1, 2, 3})); + ++finalized; + } + Require(references == 28 && retained == 24 && finalized == 8); + std::printf("FreeType face references: %d retains, %d non-final releases, %d finalized faces compared with native\n", references, retained, finalized); + } const auto root = std::filesystem::temp_directory_path() / ("anyps5_guest_freetype-" + std::to_string(std::random_device{}())); std::filesystem::create_directories(root / "app0"); CheckGasp(); @@ -801,6 +908,19 @@ int main() { GuestFaceRec* masked = nullptr; Require(FT_New_Face_nid_postfix(library, "/app0/font.ttf", 0x180000000ll, &masked) == 0 && masked != nullptr && masked->face_index == 0); Require(FT_New_Face_nid_postfix(library, "/app0/font.ttf", 0, &face) == 0 && face != nullptr && face != masked); + { + FT_Face nativeFile = nullptr; + Require(FT_New_Face(hostLibrary, "app0/font.ttf", 0, &nativeFile) == 0); + const auto* bytes = FaceOf(face)->bytes.data(); + Require(FaceOf(face)->bytes.size() == font.size() && std::memcmp(bytes, font.data(), font.size()) == 0); + Require(FT_Reference_Face_nid_postfix(face) == FT_Reference_Face(nativeFile)); + Require(FT_Done_Face(nativeFile) == 0 && FT_Done_Face_nid_postfix(face) == 0); + Require(static_cast(library)->faces.size() == 2); + Require(FaceOf(face)->bytes.data() == bytes && std::memcmp(bytes, font.data(), font.size()) == 0); + Require(FT_Get_Char_Index_nid_postfix(face, 'A') == FT_Get_Char_Index(nativeFile, 'A')); + Require(FT_Done_Face(nativeFile) == 0); + std::puts("File-backed face reference: owned font bytes survive a non-final release"); + } FaceMatches(face, host, library); const auto fontSize = static_cast(font.size()); diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 5fd44fd1d1..3fddd127ab 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -1,6 +1,7 @@ # libSceFreeType technical debt ### Unknown function info +- [FT_Reference_Face](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signature, reference-counter limits and finalizer behavior are unverified; the LP64 guest adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Counter overflow and reentrant finalization have not been measured on the console. - [FT_Get_Sfnt_Name_Count, FT_Get_Sfnt_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, record layout and errors are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftsnames.c) and assumes the public LP64 `FT_SfntName` layout (24 bytes, string pointer at 8, byte length at 16). Strings retain their original encoding, borrow native face storage and remain valid only while the face is alive From 12e3082520de70646e16e3a237ad05ebd3c16e99 Mon Sep 17 00:00:00 2001 From: Rookie <175737347+rooklz@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:15:34 +0100 Subject: [PATCH 312/428] feat(libSceCesCs): encode UCS-2, UTF-8, UTF-16 and UTF-32 characters into any MBCS profile --- core/libs/prx/libSceCesCs/Export.cpp | 47 +++- core/libs/prx/libSceCesCs/PendingExports.cpp | 40 --- core/libs/prx/libSceCesCs/include/Api.hpp | 20 +- core/libs/tests/GuestCesChinese.cpp | 7 +- core/libs/tests/GuestCesDbcs.cpp | 8 +- core/libs/tests/GuestCesJapanese.cpp | 7 +- core/libs/tests/GuestCesMbc.cpp | 272 ++++++++++++++++++- docs/dev/debt/libSceCesCs.md | 4 +- 8 files changed, 333 insertions(+), 72 deletions(-) diff --git a/core/libs/prx/libSceCesCs/Export.cpp b/core/libs/prx/libSceCesCs/Export.cpp index 94cfa2da51..15e18e9e3b 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -931,6 +931,11 @@ bool isScalar(uint32_t code) { return code <= 0x10FFFF && (code < 0xD800 || code > 0xDFFF); } +int scalarError(uint32_t code) { + if (code > 0x10FFFF) return CES_ERROR_INVALID_ENCODE; + return code >= 0xD800 && code <= 0xDFFF ? CES_ERROR_ILLEGAL_CODE : 0; +} + template int decodeUtf16(const uint16_t* utf16, uint32_t utf16max, uint32_t& length, uint32_t& code) { code = loadUnit(utf16); @@ -1018,8 +1023,14 @@ int encodeProfiled(const void* profile, uint32_t code, uint8_t* target, uint32_t int scalarToProfiled(bool (*matches)(const void*), uint32_t code, const void* profile, uint8_t* target, uint32_t targetMax, uint32_t* targetSize) { if (!matches(profile)) return CES_ERROR_INVALID_PARAMETER; if (!target) return CES_ERROR_INVALID_DST_BUFFER; - if (!isScalar(code)) return CES_ERROR_INVALID_ENCODE; - return encodeProfiled(profile, code, target, targetMax, targetSize); + int result = scalarError(code); + uint8_t encoded[4]; + uint32_t size = 0; + if (result == 0) result = encodeMbcs(profile, code, encoded, size); + if (result == 0 && targetMax < size) result = CES_ERROR_DST_BUFFER_END; + if (result == 0) std::memcpy(target, encoded, size); + if (targetSize) *targetSize = result == 0 || result == CES_ERROR_DST_BUFFER_END ? size : 0; + return result; } template @@ -1655,6 +1666,38 @@ int APS5_VABI sceCesMbcToUtf32le(const void* profile, const uint8_t* mbcBuffer, return profiledToUtf32(isMbcs, profile, mbcBuffer, mbcMax, mbcLen, utf32); } +int APS5_VABI sceCesUcs2ToMbc(uint16_t ucs2, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen) { + return scalarToProfiled(isMbcs, ucs2, profile, mbcBuffer, mbcMax, mbcLen); +} + +int APS5_VABI sceCesUtf8ToMbc(const uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Len, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen) { + return bufferToProfiled(isMbcs, utf8Buffer, utf8Max, utf8Len, profile, mbcBuffer, mbcMax, mbcLen, decodeScalarUtf8); +} + +int APS5_VABI sceCesUtf16ToMbc(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Len, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen) { + return bufferToProfiled(isMbcs, utf16Buffer, utf16Max, utf16Len, profile, mbcBuffer, mbcMax, mbcLen, decodeUtf16); +} + +int APS5_VABI sceCesUtf16beToMbc(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Len, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen) { + return bufferToProfiled(isMbcs, utf16Buffer, utf16Max, utf16Len, profile, mbcBuffer, mbcMax, mbcLen, decodeUtf16); +} + +int APS5_VABI sceCesUtf16leToMbc(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Len, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen) { + return bufferToProfiled(isMbcs, utf16Buffer, utf16Max, utf16Len, profile, mbcBuffer, mbcMax, mbcLen, decodeUtf16); +} + +int APS5_VABI sceCesUtf32ToMbc(uint32_t utf32, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen) { + return scalarToProfiled(isMbcs, utf32, profile, mbcBuffer, mbcMax, mbcLen); +} + +int APS5_VABI sceCesUtf32beToMbc(const uint32_t* utf32, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen) { + return utf32ToProfiled(isMbcs, utf32, profile, mbcBuffer, mbcMax, mbcLen); +} + +int APS5_VABI sceCesUtf32leToMbc(const uint32_t* utf32, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen) { + return utf32ToProfiled(isMbcs, utf32, profile, mbcBuffer, mbcMax, mbcLen); +} + int APS5_VABI sceCesSbcToUcs2(const uint8_t* profile, uint8_t sbc, uint16_t* ucs2) { uint32_t code = 0; if (const int result = sbcToUnit(profile, sbc, ucs2, code); result != 0) return result; diff --git a/core/libs/prx/libSceCesCs/PendingExports.cpp b/core/libs/prx/libSceCesCs/PendingExports.cpp index 849f73b092..ac98b0d50a 100644 --- a/core/libs/prx/libSceCesCs/PendingExports.cpp +++ b/core/libs/prx/libSceCesCs/PendingExports.cpp @@ -248,11 +248,6 @@ int APS5_VABI sceCesUcs2StrToIso2022Str(void) { return 0; } -int APS5_VABI sceCesUcs2ToMbc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUcsProfileInitEucJis2004(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -298,21 +293,6 @@ int APS5_VABI sceCesUtf16StrToIso2022Str(void) { return 0; } -int APS5_VABI sceCesUtf16ToMbc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUtf16beToMbc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUtf16leToMbc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32StrGetIso2022Len(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -323,21 +303,6 @@ int APS5_VABI sceCesUtf32StrToIso2022Str(void) { return 0; } -int APS5_VABI sceCesUtf32ToMbc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUtf32beToMbc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUtf32leToMbc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf8StrGetIso2022Len(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -348,9 +313,4 @@ int APS5_VABI sceCesUtf8StrToIso2022Str(void) { return 0; } -int APS5_VABI sceCesUtf8ToMbc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceCesCs/include/Api.hpp b/core/libs/prx/libSceCesCs/include/Api.hpp index 4ad8a366f1..de9d5746e4 100644 --- a/core/libs/prx/libSceCesCs/include/Api.hpp +++ b/core/libs/prx/libSceCesCs/include/Api.hpp @@ -54,8 +54,8 @@ int APS5_VABI sceCesMbcsStrGetUtf8Len(void* context, const uint8_t* mbcs, uint32 int APS5_VABI sceCesMbcsStrToUcs2Str(void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint16_t* ucs2, uint32_t ucs2max, uint32_t* ucs2Len); int APS5_VABI sceCesMbcsStrGetUcs2Len(void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint32_t* ucs2Len); int APS5_VABI sceCesEucJpToUcs2(const void* profile, const uint8_t* euc, uint32_t eucMax, uint32_t* eucSize, uint16_t* ucs2); -int APS5_VABI sceCesSbcToUtf8(const uint8_t* profile, uint8_t sbc, uint8_t* utf8, uint32_t utf8max, uint32_t* utf8Len); -int APS5_VABI sceCesUtf8ToSbc(const uint8_t* utf8, uint32_t utf8max, uint32_t* utf8Len, const uint8_t* profile, uint8_t* sbc); +int APS5_VABI sceCesSbcToUtf8(const uint8_t* profile, uint8_t sbc, uint8_t* utf8, uint32_t utf8max, uint32_t* utf8_len); +int APS5_VABI sceCesUtf8ToSbc(const uint8_t* utf8, uint32_t utf8max, uint32_t* utf8_len, const uint8_t* profile, uint8_t* sbc); int APS5_VABI sceCesEucJpToUtf8(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucLength, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length); int APS5_VABI sceCesEucKrToUtf8(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucLength, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length); int APS5_VABI sceCesBig5ToUtf8(const void* profile, const uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Length, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length); @@ -256,7 +256,7 @@ int APS5_VABI sceCesRefersUcsProfileKsX1003Tilde0x7e(void); int APS5_VABI sceCesSJisUcsProfileSetSbcs(void); int APS5_VABI sceCesUcs2StrGetIso2022Len(void); int APS5_VABI sceCesUcs2StrToIso2022Str(void); -int APS5_VABI sceCesUcs2ToMbc(void); +int APS5_VABI sceCesUcs2ToMbc(uint16_t ucs2, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen); int APS5_VABI sceCesUcsProfileInitEucJis2004(void); int APS5_VABI sceCesUcsProfileInitIso2022JpCp50221(void); int APS5_VABI sceCesUcsProfileInitIso2022JpCp50222(void); @@ -266,17 +266,17 @@ int APS5_VABI sceCesUcsToJiscs(void); int APS5_VABI sceCesUhcUcsProfileSetSbcs(void); int APS5_VABI sceCesUtf16StrGetIso2022Len(void); int APS5_VABI sceCesUtf16StrToIso2022Str(void); -int APS5_VABI sceCesUtf16ToMbc(void); -int APS5_VABI sceCesUtf16beToMbc(void); -int APS5_VABI sceCesUtf16leToMbc(void); +int APS5_VABI sceCesUtf16ToMbc(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Len, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen); +int APS5_VABI sceCesUtf16beToMbc(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Len, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen); +int APS5_VABI sceCesUtf16leToMbc(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Len, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen); int APS5_VABI sceCesUtf32StrGetIso2022Len(void); int APS5_VABI sceCesUtf32StrToIso2022Str(void); -int APS5_VABI sceCesUtf32ToMbc(void); -int APS5_VABI sceCesUtf32beToMbc(void); -int APS5_VABI sceCesUtf32leToMbc(void); +int APS5_VABI sceCesUtf32ToMbc(uint32_t utf32, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen); +int APS5_VABI sceCesUtf32beToMbc(const uint32_t* utf32, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen); +int APS5_VABI sceCesUtf32leToMbc(const uint32_t* utf32, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen); int APS5_VABI sceCesUtf8StrGetIso2022Len(void); int APS5_VABI sceCesUtf8StrToIso2022Str(void); -int APS5_VABI sceCesUtf8ToMbc(void); +int APS5_VABI sceCesUtf8ToMbc(const uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Len, const void* profile, uint8_t* mbcBuffer, uint32_t mbcMax, uint32_t* mbcLen); int APS5_VABI sceCesUcs2ToUtf8(uint16_t source, uint8_t* target, uint32_t capacity, uint32_t* targetLength); int APS5_VABI sceCesUtf8ToUcs2(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint16_t* target); int APS5_VABI sceCesUtf8ToUtf32(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint32_t* target); diff --git a/core/libs/tests/GuestCesChinese.cpp b/core/libs/tests/GuestCesChinese.cpp index 3c52beaba1..fa1490c296 100644 --- a/core/libs/tests/GuestCesChinese.cpp +++ b/core/libs/tests/GuestCesChinese.cpp @@ -12,6 +12,7 @@ static constexpr int invalidParameter = static_cast(0x805C0001); static constexpr int invalidSrcBuffer = static_cast(0x805C0010); static constexpr int srcBufferEnd = static_cast(0x805C0011); static constexpr int invalidEncode = static_cast(0x805C0014); + constexpr int outOfCodeSpace = static_cast(0x805C0015); static constexpr int unassignedCode = static_cast(0x805C0020); static constexpr int invalidDstBuffer = static_cast(0x805C0030); static constexpr int dstBufferEnd = static_cast(0x805C0031); @@ -202,8 +203,8 @@ static void CheckGb18030(const void* gb, bool revision2005, std::uint32_t twoByt std::uint8_t mbcs[4] = {1, 2, 3, 4}; produced = 0x55; - Require(sceCesUtf32ToGb(0x10000, gb, mbcs, 3, &produced) == dstBufferEnd && produced == 0x55 && std::memcmp(mbcs, "\x01\x02\x03\x04", 4) == 0); - Require(sceCesUtf32ToGb(0xD800, gb, mbcs, sizeof(mbcs), &produced) == invalidEncode); + Require(sceCesUtf32ToGb(0x10000, gb, mbcs, 3, &produced) == dstBufferEnd && produced == 4 && std::memcmp(mbcs, "\x01\x02\x03\x04", 4) == 0); + Require(sceCesUtf32ToGb(0xD800, gb, mbcs, sizeof(mbcs), &produced) == outOfCodeSpace); Require(sceCesUtf32ToGb(0x110000, gb, mbcs, sizeof(mbcs), &produced) == invalidEncode); Require(sceCesUcs2ToGb(0x80, gb, mbcs, sizeof(mbcs), &produced) == 0 && produced == 4 && std::memcmp(mbcs, "\x81\x30\x81\x30", 4) == 0); Require(sceCesUcs2ToGb(0x20AC, gb, mbcs, sizeof(mbcs), &produced) == 0 && produced == 2 && std::memcmp(mbcs, "\xA2\xE3", 2) == 0); @@ -290,7 +291,7 @@ int main() { Require(sceCesUcs2ToEucCn(0x4E02, euc, mbcs, sizeof(mbcs), &produced) == unassignedCode); Require(sceCesUcs2ToEucCn(0x20AC, euc, mbcs, sizeof(mbcs), &produced) == unassignedCode); Require(sceCesUcs2ToEucCn(0x4E2D, euc, mbcs, 1, &produced) == dstBufferEnd); - Require(sceCesUcs2ToEucCn(0xDC00, euc, mbcs, sizeof(mbcs), &produced) == invalidEncode); + Require(sceCesUcs2ToEucCn(0xDC00, euc, mbcs, sizeof(mbcs), &produced) == outOfCodeSpace); Require(sceCesUcs2ToGb(0x4E02, gbk, mbcs, sizeof(mbcs), &produced) == 0 && produced == 2 && std::memcmp(mbcs, "\x81\x40", 2) == 0); Require(sceCesUcs2ToGb(0x20AC, gbk, mbcs, sizeof(mbcs), &produced) == 0 && produced == 1 && mbcs[0] == 0x80); Require(sceCesUcs2ToGb(0x3042, euc, mbcs, sizeof(mbcs), &produced) == invalidParameter); diff --git a/core/libs/tests/GuestCesDbcs.cpp b/core/libs/tests/GuestCesDbcs.cpp index 16d137f97e..e8c093f13b 100644 --- a/core/libs/tests/GuestCesDbcs.cpp +++ b/core/libs/tests/GuestCesDbcs.cpp @@ -238,7 +238,7 @@ static void RequireEncode(const Encoders& encoders, const void* profile, std::ui Encoded out; Require(Encode(encoders, form, code, profile, out) == expected); if (expected == 0) Require(out.read == SourceUnits(form, code) && out.size == size && std::memcmp(out.bytes, bytes, size) == 0); - else Require(out.read == 77 && out.size == 77 && out.bytes[0] == 0x55 && out.bytes[1] == 0x55); + else Require(out.read == 77 && out.size == ((form == 0 || form >= 5) && expected != static_cast(0x805C0001) ? 0u : 77u) && out.bytes[0] == 0x55 && out.bytes[1] == 0x55); } } @@ -376,9 +376,9 @@ int main() { const std::uint32_t big = 0x0000AC00; Require(sceCesUcs2ToEucKr(0xAC00, uhc, nullptr, 0, nullptr) == invalidParameter); Require(sceCesUcs2ToEucKr(0xD800, eucKr, nullptr, 0, nullptr) == invalidDstBuffer); - Require(sceCesUcs2ToEucKr(0xD800, eucKr, out, 2, &size) == invalidEncode); + Require(sceCesUcs2ToEucKr(0xD800, eucKr, out, 2, &size) == illegalCode); Require(sceCesUtf32ToUhc(0x110000, uhc, out, 2, &size) == invalidEncode); - Require(sceCesUtf32ToUhc(0xDFFF, uhc, out, 2, &size) == invalidEncode); + Require(sceCesUtf32ToUhc(0xDFFF, uhc, out, 2, &size) == illegalCode); Require(sceCesUtf32beToBig5(nullptr, eucKr, nullptr, 0, nullptr) == invalidParameter); Require(sceCesUtf32beToBig5(nullptr, big5, nullptr, 0, nullptr) == invalidSrcBuffer); Require(sceCesUtf32leToBig5(&big, big5, nullptr, 0, nullptr) == invalidDstBuffer); @@ -394,7 +394,7 @@ int main() { Require(sceCesUtf16ToUhc(lowSurrogate, 1, &read, uhc, out, 2, &size) == invalidEncode); Require(sceCesUtf16ToUhc(highSurrogate, 1, &read, uhc, out, 2, &size) == srcBufferEnd); Require(sceCesUtf16ToUhc(highSurrogate, 2, &read, uhc, out, 2, &size) == invalidEncode); - Require(out[0] == 0x55 && out[1] == 0x55 && size == 77 && read == 77); + Require(out[0] == 0x55 && out[1] == 0x55 && size == 0 && read == 77); std::memcpy(&unit, "\xAC\x00", 2); Require(sceCesUtf16beToEucKr(&unit, 1, &read, eucKr, out, 2, &size) == 0 && read == 1 && size == 2 && std::memcmp(out, "\xB0\xA1", 2) == 0); diff --git a/core/libs/tests/GuestCesJapanese.cpp b/core/libs/tests/GuestCesJapanese.cpp index 6a8af50468..30bfa8b235 100644 --- a/core/libs/tests/GuestCesJapanese.cpp +++ b/core/libs/tests/GuestCesJapanese.cpp @@ -15,6 +15,7 @@ static const std::uint8_t* Bytes(const char* text) { return reinterpret_cast(0x805C0001); constexpr int srcBufferEnd = static_cast(0x805C0011); constexpr int invalidEncode = static_cast(0x805C0014); +constexpr int outOfCodeSpace = static_cast(0x805C0015); constexpr int unassignedCode = static_cast(0x805C0020); constexpr int dstBufferEnd = static_cast(0x805C0031); constexpr std::uint32_t jisX0208Cells = 6878; @@ -65,7 +66,7 @@ static bool EncodeLeavesOutputs(int expected, const void* profile, std::uint16_t std::uint8_t mbcs[4] = {0xEE, 0xEE, 0xEE, 0xEE}; std::uint32_t size = 77; const int result = sjis ? sceCesUcs2ToSJis(ucs2, profile, mbcs, sizeof(mbcs), &size) : sceCesUcs2ToEucJp(ucs2, profile, mbcs, sizeof(mbcs), &size); - return result == expected && size == 77 && mbcs[0] == 0xEE && mbcs[1] == 0xEE && mbcs[2] == 0xEE; + return result == expected && size == (expected == static_cast(0x805C0001) ? 77u : 0u) && mbcs[0] == 0xEE && mbcs[1] == 0xEE && mbcs[2] == 0xEE; } static bool DecodeFails(int expected, const void* profile, const char* bytes, std::uint32_t size, bool sjis) { @@ -166,7 +167,7 @@ static void TestJisX0208Profiles(const void* cp932, const void* cp51932) { Require(DecodeFails(srcBufferEnd, profile, "\xA4\xA2", 1, false)); std::uint8_t mbcs[4] = {0xEE, 0xEE, 0xEE, 0xEE}; std::uint32_t size = 77; - Require(sceCesUcs2ToEucJp(0x3042, profile, mbcs, 1, &size) == dstBufferEnd && size == 77 && mbcs[0] == 0xEE); + Require(sceCesUcs2ToEucJp(0x3042, profile, mbcs, 1, &size) == dstBufferEnd && size == 2 && mbcs[0] == 0xEE); } Require(EucToUcs2(cp51932, "\xA1\xC0", 2, ucs2) == 0 && ucs2 == 0xFF3C && EucEncodes(cp51932, 0xFF3C, "\xA1\xC0")); Require(EucToUcs2(cp51932, "\xA1\xC1", 2, ucs2) == 0 && ucs2 == 0xFF5E && EucToUcs2(cp51932, "\xAD\xA1", 2, ucs2) == 0 && ucs2 == 0x2460); @@ -304,7 +305,7 @@ int main() { Require(sceCesUcs2ToSJis(0x3042, sjis, mbcs, sizeof(mbcs), &produced) == 0 && produced == 2 && std::memcmp(mbcs, "\x82\xA0", 2) == 0); Require(sceCesUcs2ToSJis(0xFF71, sjis, mbcs, sizeof(mbcs), &produced) == 0 && produced == 1 && mbcs[0] == 0xB1); Require(sceCesUcs2ToSJis(0x3042, sjis, mbcs, 1, &produced) == dstBufferEnd); - Require(sceCesUcs2ToSJis(0xD800, sjis, mbcs, sizeof(mbcs), &produced) == invalidEncode); + Require(sceCesUcs2ToSJis(0xD800, sjis, mbcs, sizeof(mbcs), &produced) == outOfCodeSpace); Require(sceCesUcs2ToSJis(0x4E02, sjis, mbcs, sizeof(mbcs), &produced) == unassignedCode); Require(sceCesUcs2ToSJis(0x3042, euc, mbcs, sizeof(mbcs), &produced) == invalidParameter); Require(sceCesUcs2ToSJis(0x3042, sceCesRefersUcsProfileCp1252(), mbcs, sizeof(mbcs), &produced) == invalidParameter); diff --git a/core/libs/tests/GuestCesMbc.cpp b/core/libs/tests/GuestCesMbc.cpp index 4bab69f25e..e88d2f94d6 100644 --- a/core/libs/tests/GuestCesMbc.cpp +++ b/core/libs/tests/GuestCesMbc.cpp @@ -1,13 +1,201 @@ #include "prx/libSceCesCs/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include +constexpr int invalidParameter = static_cast(0x805C0001); +constexpr int invalidSrcBuffer = static_cast(0x805C0010); +constexpr int srcBufferEnd = static_cast(0x805C0011); +constexpr int invalidEncode = static_cast(0x805C0014); +constexpr int outOfCodeSpace = static_cast(0x805C0015); +constexpr int unassignedCode = static_cast(0x805C0020); +constexpr int unsupported = static_cast(0x805C0024); +constexpr int invalidDstBuffer = static_cast(0x805C0030); +constexpr int dstBufferEnd = static_cast(0x805C0031); +constexpr std::uint32_t untouched = 0xEEEEEEEE; + +enum Form { Ucs2, Utf8, Utf16, Utf16be, Utf16le, Utf32, Utf32be, Utf32le, FormCount }; + +struct Sequence { + std::uint32_t count; + std::uint32_t units[4]; + int expected; +}; + static void Require(bool value) { if (!value) std::abort(); } static const std::uint8_t* Bytes(const char* text) { return reinterpret_cast(text); } +static bool IsBuffer(int form) { return form == Utf8 || form == Utf16 || form == Utf16be || form == Utf16le; } + +static std::uint32_t Units(int form, std::uint32_t code, std::uint32_t* units) { + constexpr std::uint32_t leads[5] = {0, 0, 0xC0, 0xE0, 0xF0}; + if (form == Utf8 && code >= 0x80) { + const std::uint32_t count = code < 0x800 ? 2 : code < 0x10000 ? 3 : 4; + for (std::uint32_t i = count - 1; i > 0; --i, code >>= 6) units[i] = 0x80 | (code & 0x3F); + units[0] = leads[count] | code; + return count; + } + if (IsBuffer(form) && code >= 0x10000) { + units[0] = 0xD800 + ((code - 0x10000) >> 10); + units[1] = 0xDC00 + ((code - 0x10000) & 0x3FF); + return 2; + } + units[0] = code; + return 1; +} + +static int Call(int form, const std::uint32_t* units, std::uint32_t count, const void* profile, std::uint8_t* mbc, std::uint32_t mbcMax, std::uint32_t* mbcLen, std::uint32_t* sourceLen) { + const bool bigEndian = form == Utf16be || form == Utf32be || ((form == Utf16 || form == Utf32) && std::endian::native == std::endian::big); + const std::uint32_t width = form == Utf8 ? 1 : form >= Utf32 ? 4 : 2; + alignas(4) std::uint8_t bytes[16] = {}; + for (std::uint32_t i = 0; units && i < count; ++i) { + for (std::uint32_t b = 0; b < width; ++b) bytes[i * width + b] = static_cast(units[i] >> (8 * (bigEndian ? width - 1 - b : b))); + } + const std::uint8_t* utf8 = units ? bytes : nullptr; + const auto* utf16 = reinterpret_cast(utf8); + const auto* utf32 = reinterpret_cast(utf8); + const std::uint32_t value = units ? units[0] : 0; + switch (form) { + case Ucs2: return sceCesUcs2ToMbc(static_cast(value), profile, mbc, mbcMax, mbcLen); + case Utf8: return sceCesUtf8ToMbc(utf8, count, sourceLen, profile, mbc, mbcMax, mbcLen); + case Utf16: return sceCesUtf16ToMbc(utf16, count, sourceLen, profile, mbc, mbcMax, mbcLen); + case Utf16be: return sceCesUtf16beToMbc(utf16, count, sourceLen, profile, mbc, mbcMax, mbcLen); + case Utf16le: return sceCesUtf16leToMbc(utf16, count, sourceLen, profile, mbc, mbcMax, mbcLen); + case Utf32: return sceCesUtf32ToMbc(value, profile, mbc, mbcMax, mbcLen); + case Utf32be: return sceCesUtf32beToMbc(utf32, profile, mbc, mbcMax, mbcLen); + default: return sceCesUtf32leToMbc(utf32, profile, mbc, mbcMax, mbcLen); + } +} + +static void RequireFailure(int form, const std::uint32_t* units, std::uint32_t count, const void* profile, bool target, std::uint32_t mbcMax, int expected) { + std::uint8_t mbc[4] = {0xEE, 0xEE, 0xEE, 0xEE}; + std::uint32_t mbcLen = untouched; + std::uint32_t sourceLen = untouched; + Require(Call(form, units, count, profile, target ? mbc : nullptr, mbcMax, &mbcLen, &sourceLen) == expected); + const bool refused = !IsBuffer(form) && expected != invalidParameter && expected != invalidSrcBuffer && expected != invalidDstBuffer; + Require(mbc[0] == 0xEE && mbc[1] == 0xEE && mbc[2] == 0xEE && mbc[3] == 0xEE && mbcLen == (refused ? 0 : untouched) && sourceLen == untouched); +} + +static void RequireAll(std::uint32_t code, const void* profile, int expected, const std::uint8_t* bytes, std::uint32_t size) { + for (int form = code > 0xFFFF ? Utf8 : Ucs2; form < FormCount; ++form) { + std::uint32_t units[4] = {}; + const std::uint32_t count = Units(form, code, units); + if (expected != 0) { + RequireFailure(form, units, count, profile, true, 4, expected); + continue; + } + std::uint8_t mbc[4] = {0xEE, 0xEE, 0xEE, 0xEE}; + std::uint32_t mbcLen = untouched; + std::uint32_t sourceLen = untouched; + Require(Call(form, units, count, profile, mbc, sizeof(mbc), &mbcLen, &sourceLen) == 0); + Require(mbcLen == size && std::memcmp(mbc, bytes, size) == 0 && sourceLen == (IsBuffer(form) ? count : untouched)); + } +} + +static std::uint32_t SweepCodes(const void* profile) { + constexpr std::uint32_t supplementary[] = {0x10000, 0x1F600, 0x10FFFF}; + std::uint32_t mapped = 0; + for (std::uint32_t code = 0; code < 0x10000; ++code) { + if (code >= 0xD800 && code <= 0xDFFF) continue; + std::uint8_t bytes[4] = {}; + std::uint32_t size = 0; + const int result = sceCesUtf32ToMbc(code, profile, bytes, sizeof(bytes), &size); + Require(result == 0 || result == unassignedCode || result == unsupported); + if (result == 0 && size > 0) { + std::uint32_t consumed = 0; + std::uint32_t back = 0; + Require(sceCesMbcToUtf32(profile, bytes, size, &consumed, &back) == 0 && consumed == size); + if (back == code) ++mapped; + } + RequireAll(code, profile, result, bytes, size); + } + for (const std::uint32_t code : supplementary) RequireAll(code, profile, unassignedCode, nullptr, 0); + return mapped; +} + +static std::uint32_t RoundTrip(const void* profile, const std::uint8_t* source, std::uint32_t size) { + std::uint32_t consumed = 0; + std::uint32_t produced = 0; + std::uint32_t code = 0; + std::uint16_t ucs2 = 0; + std::uint8_t utf8[4] = {}; + std::uint32_t utf8Size = 0; + std::uint16_t utf16[3] = {}; + std::uint32_t utf32[2] = {}; + Require(sceCesMbcToUtf32(profile, source, size, &consumed, &code) == 0 && consumed == size); + Require(sceCesMbcToUcs2(profile, source, size, &consumed, &ucs2) == 0 && consumed == size); + Require(sceCesMbcToUtf8(profile, source, size, &consumed, utf8, sizeof(utf8), &utf8Size) == 0 && consumed == size); + Require(sceCesMbcToUtf16(profile, source, size, &consumed, &utf16[0], 1, &produced) == 0 && consumed == size); + Require(sceCesMbcToUtf16be(profile, source, size, &consumed, &utf16[1], 1, &produced) == 0 && consumed == size); + Require(sceCesMbcToUtf16le(profile, source, size, &consumed, &utf16[2], 1, &produced) == 0 && consumed == size); + Require(sceCesMbcToUtf32be(profile, source, size, &consumed, &utf32[0]) == 0 && consumed == size); + Require(sceCesMbcToUtf32le(profile, source, size, &consumed, &utf32[1]) == 0 && consumed == size); + std::uint8_t mbc[8][4] = {}; + std::uint32_t mbcLen[8] = {}; + std::uint32_t sourceLen[4] = {}; + Require(sceCesUtf32ToMbc(code, profile, mbc[0], 4, &mbcLen[0]) == 0); + Require(sceCesUcs2ToMbc(ucs2, profile, mbc[1], 4, &mbcLen[1]) == 0); + Require(sceCesUtf8ToMbc(utf8, utf8Size, &sourceLen[0], profile, mbc[2], 4, &mbcLen[2]) == 0 && sourceLen[0] == utf8Size); + Require(sceCesUtf16ToMbc(&utf16[0], 1, &sourceLen[1], profile, mbc[3], 4, &mbcLen[3]) == 0 && sourceLen[1] == 1); + Require(sceCesUtf16beToMbc(&utf16[1], 1, &sourceLen[2], profile, mbc[4], 4, &mbcLen[4]) == 0 && sourceLen[2] == 1); + Require(sceCesUtf16leToMbc(&utf16[2], 1, &sourceLen[3], profile, mbc[5], 4, &mbcLen[5]) == 0 && sourceLen[3] == 1); + Require(sceCesUtf32beToMbc(&utf32[0], profile, mbc[6], 4, &mbcLen[6]) == 0); + Require(sceCesUtf32leToMbc(&utf32[1], profile, mbc[7], 4, &mbcLen[7]) == 0); + for (std::uint32_t i = 1; i < 8; ++i) Require(mbcLen[i] == mbcLen[0] && std::memcmp(mbc[i], mbc[0], mbcLen[0]) == 0); + return mbcLen[0] == size && std::memcmp(mbc[0], source, size) == 0 ? 1 : 0; +} + +static std::uint32_t SweepBytes(const void* profile) { + std::uint32_t identical = 0; + std::uint8_t source[3] = {}; + std::uint32_t consumed = 0; + std::uint32_t code = 0; + for (std::uint32_t lead = 0; lead < 256; ++lead) { + source[0] = static_cast(lead); + const int single = sceCesMbcToUtf32(profile, source, 1, &consumed, &code); + if (single == 0) identical += RoundTrip(profile, source, 1); + if (single != srcBufferEnd) continue; + for (std::uint32_t trail = 0; trail < 256; ++trail) { + source[1] = static_cast(trail); + const int pair = sceCesMbcToUtf32(profile, source, 2, &consumed, &code); + if (pair == 0) identical += RoundTrip(profile, source, 2); + for (std::uint32_t third = 0; pair == srcBufferEnd && third < 256; ++third) { + source[2] = static_cast(third); + if (sceCesMbcToUtf32(profile, source, 3, &consumed, &code) == 0) identical += RoundTrip(profile, source, 3); + } + } + } + return identical; +} + +static void RequireInvalid(const void* profile) { + constexpr int scalarForms[] = {Ucs2, Utf32, Utf32be, Utf32le}; + const Sequence utf8[] = { + {2, {0xC0, 0x80}, outOfCodeSpace}, {2, {0xC1, 0xBF}, outOfCodeSpace}, {3, {0xE0, 0x80, 0x80}, outOfCodeSpace}, + {3, {0xE0, 0x9F, 0xBF}, outOfCodeSpace}, {4, {0xF0, 0x80, 0x80, 0x80}, outOfCodeSpace}, {4, {0xF0, 0x8F, 0xBF, 0xBF}, outOfCodeSpace}, + {3, {0xED, 0xA0, 0x80}, outOfCodeSpace}, {3, {0xED, 0xBF, 0xBF}, outOfCodeSpace}, {4, {0xF4, 0x90, 0x80, 0x80}, unassignedCode}, + {4, {0xF7, 0xBF, 0xBF, 0xBF}, unassignedCode}, {1, {0x80}, invalidEncode}, {1, {0xFF}, invalidEncode}, + {3, {0xE3, 0x41, 0x42}, invalidEncode}, {2, {0xE3, 0x81}, srcBufferEnd}, {3, {0xF0, 0x9F, 0x98}, srcBufferEnd}, + }; + const Sequence utf16[] = { + {1, {0xDC00}, invalidEncode}, {1, {0xDFFF}, invalidEncode}, {2, {0xD800, 0x0041}, invalidEncode}, + {2, {0xDBFF, 0xDBFF}, invalidEncode}, {2, {0xDC00, 0xD800}, invalidEncode}, {1, {0xD800}, srcBufferEnd}, + }; + const Sequence utf32[] = {{1, {0xD800}, outOfCodeSpace}, {1, {0xDFFF}, outOfCodeSpace}, {1, {0x110000}, invalidEncode}, {1, {0xFFFFFFFF}, invalidEncode}}; + for (const Sequence& sequence : utf8) RequireFailure(Utf8, sequence.units, sequence.count, profile, true, 4, sequence.expected); + for (const Sequence& sequence : utf16) { + for (int form = Utf16; form <= Utf16le; ++form) RequireFailure(form, sequence.units, sequence.count, profile, true, 4, sequence.expected); + } + for (const Sequence& sequence : utf32) { + for (const int form : scalarForms) { + if (form != Ucs2 || sequence.units[0] <= 0xFFFF) RequireFailure(form, sequence.units, sequence.count, profile, true, 4, sequence.expected); + } + } +} + static void RequireX0213(std::uint16_t sjis, std::uint8_t men, std::uint8_t ku, std::uint8_t ten) { std::uint8_t m = 0; std::uint8_t k = 0; @@ -18,14 +206,6 @@ static void RequireX0213(std::uint16_t sjis, std::uint8_t men, std::uint8_t ku, } int main() { - constexpr int invalidParameter = static_cast(0x805C0001); - constexpr int invalidSrcBuffer = static_cast(0x805C0010); - constexpr int srcBufferEnd = static_cast(0x805C0011); - constexpr int invalidEncode = static_cast(0x805C0014); - constexpr int unassignedCode = static_cast(0x805C0020); - constexpr int invalidDstBuffer = static_cast(0x805C0030); - constexpr int dstBufferEnd = static_cast(0x805C0031); - alignas(8) std::uint8_t sjisSheet[64] = {}; alignas(8) std::uint8_t eucSheet[64] = {}; alignas(8) std::uint8_t gbkSheet[64] = {}; @@ -132,4 +312,80 @@ int main() { Require(std::memcmp(utf8, "\xE4\xB8\xAD", 3) == 0); Require(sceCesMbcsUcsContextInitCopy(nullptr, context) == invalidParameter); Require(sceCesMbcsUcsContextInitCopy(copy, nullptr) == invalidParameter); + + alignas(8) std::uint8_t eucCnSheet[64] = {}; + alignas(8) std::uint8_t eucKrSheet[64] = {}; + alignas(8) std::uint8_t uhcSheet[64] = {}; + alignas(8) std::uint8_t big5Sheet[64] = {}; + const void* eucCn = sceCesUcsProfileInitEucCnGb2312(eucCnSheet); + const void* eucKr = sceCesUcsProfileInitEucKr(eucKrSheet); + const void* uhc = sceCesUcsProfileInitUhc(uhcSheet); + const void* big5 = sceCesUcsProfileInitBig5Cp950(big5Sheet); + const void* cp1251 = sceCesRefersUcsProfileCp1251(); + const void* cp1250 = sceCesRefersUcsProfileCp1250(); + + RequireAll(0x20AC, cp1252, 0, Bytes("\x80"), 1); + RequireAll(0x00E9, cp1252, 0, Bytes("\xE9"), 1); + RequireAll(0x0100, cp1252, unassignedCode, nullptr, 0); + RequireAll(0x0416, cp1251, 0, Bytes("\xC6"), 1); + RequireAll(0x0141, cp1250, 0, Bytes("\xA3"), 1); + RequireAll(0x3042, sjis, 0, Bytes("\x82\xA0"), 2); + RequireAll(0xFF71, sjis, 0, Bytes("\xB1"), 1); + RequireAll(0x4E02, sjis, unassignedCode, nullptr, 0); + RequireAll(0x3042, euc, 0, Bytes("\xA4\xA2"), 2); + RequireAll(0xFF71, euc, 0, Bytes("\x8E\xB1"), 2); + RequireAll(0x4E02, euc, unassignedCode, nullptr, 0); + RequireAll(0x4E2D, gbk, 0, Bytes("\xD6\xD0"), 2); + RequireAll(0x4E02, gbk, 0, Bytes("\x81\x40"), 2); + RequireAll(0x20AC, gbk, 0, Bytes("\x80"), 1); + RequireAll(0x4E2D, eucCn, 0, Bytes("\xD6\xD0"), 2); + RequireAll(0x4E02, eucCn, unassignedCode, nullptr, 0); + RequireAll(0xAC00, eucKr, 0, Bytes("\xB0\xA1"), 2); + RequireAll(0xAC02, eucKr, unassignedCode, nullptr, 0); + RequireAll(0xAC02, uhc, 0, Bytes("\x81\x41"), 2); + RequireAll(0x4E00, big5, 0, Bytes("\xA4\x40"), 2); + + std::uint32_t units[4] = {}; + for (int form = Ucs2; form < FormCount; ++form) { + std::uint32_t count = Units(form, 0xDC00, units); + RequireFailure(form, nullptr, 0, nullptr, false, 0, invalidParameter); + RequireFailure(form, units, count, blankSheet, true, 4, invalidParameter); + if (form != Ucs2 && form != Utf32) RequireFailure(form, nullptr, 0, sjis, false, 0, invalidSrcBuffer); + if (IsBuffer(form)) RequireFailure(form, units, 0, sjis, false, 0, srcBufferEnd); + RequireFailure(form, units, count, sjis, false, 0, invalidDstBuffer); + RequireFailure(form, units, count, sjis, true, 0, IsBuffer(form) && form != Utf8 ? invalidEncode : outOfCodeSpace); + count = Units(form, 0x4E02, units); + RequireFailure(form, units, count, sjis, true, 0, unassignedCode); + count = Units(form, 0x3042, units); + std::uint8_t full[4] = {0xEE, 0xEE, 0xEE, 0xEE}; + std::uint32_t needed = untouched; + std::uint32_t sourceLen = untouched; + Require(Call(form, units, count, sjis, full, 1, &needed, &sourceLen) == dstBufferEnd && needed == (IsBuffer(form) ? untouched : 2) && full[0] == 0xEE && sourceLen == untouched); + std::uint8_t mbc[4] = {}; + Require(Call(form, units, count, sjis, mbc, sizeof(mbc), nullptr, nullptr) == 0 && std::memcmp(mbc, "\x82\xA0", 2) == 0); + } + + std::uint8_t mbc[4] = {}; + std::uint32_t wide = 0; + std::memcpy(&wide, "\x00\x00\x30\x42", 4); + Require(sceCesUtf32beToMbc(&wide, sjis, mbc, sizeof(mbc), &produced) == 0 && produced == 2 && std::memcmp(mbc, "\x82\xA0", 2) == 0); + Require(sceCesUtf32leToMbc(&wide, sjis, mbc, sizeof(mbc), &produced) == invalidEncode); + std::uint16_t narrow = 0; + std::memcpy(&narrow, "\xD8\x00", 2); + Require(sceCesUtf16leToMbc(&narrow, 1, &consumed, cp1252, mbc, sizeof(mbc), &produced) == 0 && consumed == 1 && produced == 1 && mbc[0] == 0xD8); + Require(sceCesUtf16beToMbc(&narrow, 1, &consumed, cp1252, mbc, sizeof(mbc), &produced) == srcBufferEnd); + Require(sceCesUtf8ToMbc(Bytes("A\xE4\xB8\xAD"), 4, &consumed, gbk, mbc, sizeof(mbc), &produced) == 0 && consumed == 1 && produced == 1 && mbc[0] == 'A'); + + const void* singleByte[] = {cp1252, cp1251, cp1250}; + constexpr std::uint32_t singleByteMapped[] = {251, 255, 251}; + const void* multiByte[] = {sjis, euc, eucCn, gbk, eucKr, uhc, big5}; + for (std::size_t index = 0; index < 3; ++index) { + Require(SweepCodes(singleByte[index]) == singleByteMapped[index] && SweepBytes(singleByte[index]) == singleByteMapped[index]); + RequireInvalid(singleByte[index]); + } + for (const void* profile : multiByte) { + const std::uint32_t mapped = SweepCodes(profile); + Require(mapped > 256 && SweepBytes(profile) == mapped); + RequireInvalid(profile); + } } diff --git a/docs/dev/debt/libSceCesCs.md b/docs/dev/debt/libSceCesCs.md index 29c86fc543..6905596b0a 100644 --- a/docs/dev/debt/libSceCesCs.md +++ b/docs/dev/debt/libSceCesCs.md @@ -19,7 +19,7 @@ - [sceCesSbcToUtf8 and sceCesUtf8ToSbc](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) follow a payload process on a PS5 (firmware 13.60): the three tables match the console byte for byte, with Cp1252 bytes 0x81, 0x8D, 0x8F, 0x90 and 0x9D unassigned; a null profile returns `0x805C0004`; and the length and output are cleared first, as the console does. `sceCesUtf8ToSbc` decodes leads up to 0xFD (5- and 6-byte forms), returns `0x805C0015` for overlong forms and surrogates and reports the measured lengths. The console reads any other profile pointer as a table, where these return the invalid-parameter error; which of a null profile and a scalar above U+10FFFF it reports first is not measured - [MBCS string conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - `sceCesUtf8StrToMbcsStr`, `sceCesUcs2StrToMbcsStr`, `sceCesMbcsStrToUtf8Str`, `sceCesMbcsStrToUcs2Str` and their `Get*Len` forms follow a payload process on a PS5 (firmware 13.60) with the Cp1252 and EUC-JP profiles: the lengths are cleared and a terminator is written first, then the context, the source and the output are checked in that order; a source limit of 0 reads up to the terminating zero; the output keeps room for a terminator; and on an error the characters before it stay converted, terminated and counted. They decode UTF-8 as `sceCesUtf8ToSbc` does, and a UTF-16 surrogate answers `0x805C0015`. The UCS-2 forms and the other profiles were not measured beyond these cases. A zero-filled context converted nothing and returned 0 on the console, where this returns the invalid-parameter error - [EUC-JP and Shift-JIS encoding](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) with the CP51932 and CP932 profiles gives the result, lengths and bytes a payload process read on a PS5 (firmware 13.60) for every UCS-2 value through `sceCesUcs2StrToMbcsStr` (the test pins a hash of both sweeps): the JIS forms U+00A2, U+00A3, U+00A5, U+00AC, U+2014, U+2016, U+203E, U+2212 and U+301C encode to the cells of their Microsoft forms, U+FEFF is consumed and writes nothing, EUC-JP writes U+0080-U+009F other than U+008E and U+008F as single bytes and answers `UNASSIGNED_CODE` for U+000E, and Shift-JIS answers `0x805C0024` for U+0080-U+009F. Decoding those bytes back is unchanged -- [Generic MBCS character and Shift-JIS code helpers](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - `sceCesMbcTo*` convert one character with any profile the MBCS context accepts, as the per-encoding forms above do. `sceCesSJisGetCode` and `sceCesSJisPutCode` read and write one raw Shift-JIS code (one byte for ASCII and `0xA1`-`0xDF`) without checking that it is assigned. `sceCesSJisCodeToJisX0213` and `sceCesJisX0213ToSJisCode` use the Shift_JIS-2004 arithmetic of JIS X 0213 annex 1 (plane 2 rows 1, 3-5, 8, 12-15 and 78-94 on leads `0xF0`-`0xFC`). `sceCesMbcsUcsContextInitCopy` copies the stored profile pointer. Signatures follow the Orbital type database linked above; errors, their precedence and the optional size pointers follow the other converters and are unverified against the console +- [Generic MBCS character and Shift-JIS code helpers](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the UTF-8 and UTF-16 buffer forms' errors and size pointers follow the other converters, unverified. The single-character encoders were measured on a PS5 (`sceCesUtf32ToMbc` over U+0000-U+110001 with each MBCS profile, each encoding's `Ucs2To*` and `Utf32To*` on surrogates and a one-byte target): 0x805C0015 for a surrogate, 0x805C0014 past U+10FFFF, the needed length with `DST_BUFFER_END` and a zero length with any other refusal. `sceCesMbcTo*` and `sceCes*ToMbc` convert one character with any profile the MBCS context accepts; an unmapped character answers `UNASSIGNED_CODE`. `sceCesSJisGetCode`/`PutCode` move one raw Shift-JIS code unchecked; `sceCesSJisCodeToJisX0213` and back use JIS X 0213 annex 1 arithmetic. `sceCesMbcsUcsContextInitCopy` copies the profile pointer. Signatures follow Orbital - [Single-byte profiles](../../../core/libs/prx/libSceCesCs/SingleByte.hpp) (libSceCesCs) - the console's tables for these 27 profiles are not verified. Each returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with bytes 0x00-0x7F unchanged and the high half of a public table: ISO 8859-1, -2, -4, -5, -9, -10, -13, -14, -15 and -16 from [unicode.org](https://unicode.org/Public/MAPPINGS/ISO8859/), code pages 437, 737, 775, 850, 852, 855, 860-863, 865 and 866 from [Microsoft's PC tables](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/PC/), KOI8-R and KOI8-U from [unicode.org](https://unicode.org/Public/MAPPINGS/VENDORS/MISC/), and 1254, 1256 and 1258 from Microsoft's [best-fit tables](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/), whose unassigned bytes map to C1 controls as in Cp1250. Profiles whose public table leaves bytes undefined or maps them to private use still throw -- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 70 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. +- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 62 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. - [EUC-KR, UHC and Big5 conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the tables, errors and Big5 duplicate choices are assumed, not measured. `sceCesUcsProfileInitEucKr`, `sceCesUcsProfileInitUhc` and `sceCesUcsProfileInitBig5Cp950` tag the caller's sheet like the EUC-JP profile. The tables are Microsoft's [CP949.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP949.TXT) and [CP950.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP950.TXT); EUC-KR is CP949 limited to bytes `0xA1`-`0xFE`. Bytes below `0x80` are ASCII; the KS X 1003 profile (`sceCes*UcsProfileSetSbcs`) is not implemented. User-defined areas (Private Use Area on Windows) answer `UNASSIGNED_CODE`. Big5 encoding takes the lower of two cells except for U+2550, U+255E, U+2561, U+256A, U+5341 and U+5345, as code page 950 does. The scalar calls to and from UCS-2 and UTF-8/16/32 convert one character as the Shift-JIS and EUC-JP scalar conversions do From 0e5478c6bfaee60b92519e0c73ec2c582983fd96 Mon Sep 17 00:00:00 2001 From: slopgobblercobbler Date: Sun, 25 Oct 2026 02:58:24 +0100 Subject: [PATCH 313/428] fix(libSceUserService): validate logged-in status for user accessibility and age queries --- core/libs/prx/libSceUserService/Export.cpp | 15 +++++++++---- core/libs/tests/GuestUserService.cpp | 25 ++++++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceUserService/Export.cpp b/core/libs/prx/libSceUserService/Export.cpp index 03a44d4920..fa8edb8b62 100644 --- a/core/libs/prx/libSceUserService/Export.cpp +++ b/core/libs/prx/libSceUserService/Export.cpp @@ -30,9 +30,17 @@ int UserIndex(int userId) { return -1; } -int ReportAccessibilitySetting(int user_id, int32_t* setting, int32_t value) { - const int index = UserIndex(user_id); +bool IsUserLoggedIn(int userId) { + const int index = UserIndex(userId); if (index < 0) { + return false; + } + std::lock_guard lock(userMutex); + return userConnected[index]; +} + +int ReportAccessibilitySetting(int user_id, int32_t* setting, int32_t value) { + if (!IsUserLoggedIn(user_id)) { return USER_SERVICE_ERROR_NOT_LOGGED_IN; } if (setting == nullptr) { @@ -151,8 +159,7 @@ int APS5_VABI sceUserServiceGetAccessibilityZoomFollowFocus(int user_id, int32_t } int APS5_VABI sceUserServiceGetAgeLevel(int user_id, uint32_t* age_level) { - const int index = UserIndex(user_id); - if (index < 0) { + if (!IsUserLoggedIn(user_id)) { return USER_SERVICE_ERROR_NOT_LOGGED_IN; } if (age_level == nullptr) { diff --git a/core/libs/tests/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp index d42de627a9..9b46dffc13 100644 --- a/core/libs/tests/GuestUserService.cpp +++ b/core/libs/tests/GuestUserService.cpp @@ -179,6 +179,15 @@ int main() { Require(sceUserServiceGetUserColor(USER_SERVICE_INITIAL_USER_ID + 1, &color) == USER_SERVICE_OK); Require(color == 1); + Require(sceUserServiceGetAgeLevel(USER_SERVICE_INITIAL_USER_ID + 1, &age_level) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + Require(sceUserServiceGetAgeLevel(USER_SERVICE_INITIAL_USER_ID + 1, nullptr) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + for (const Setting& setting : settings) { + std::int32_t value = 0x5a5a5a5a; + Require(setting.get(USER_SERVICE_INITIAL_USER_ID + 1, &value) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + Require(value == 0x5a5a5a5a); + Require(setting.get(USER_SERVICE_INITIAL_USER_ID + 1, nullptr) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + } + UserServiceSetUserConnected_nid_postfix(1, true); Require(sceUserServiceGetRegisteredUserIdList(®istered) == USER_SERVICE_OK); Require(registered.user_id[0] == USER_SERVICE_INITIAL_USER_ID); @@ -186,10 +195,26 @@ int main() { for (int i = 2; i < 16; ++i) { Require(registered.user_id[i] == USER_SERVICE_USER_ID_INVALID); } + Require(sceUserServiceGetAgeLevel(USER_SERVICE_INITIAL_USER_ID + 1, &age_level) == USER_SERVICE_OK); + Require(age_level == 0xffffffffu); + for (const Setting& setting : settings) { + std::int32_t value = 0x5a5a5a5a; + Require(setting.get(USER_SERVICE_INITIAL_USER_ID + 1, &value) == USER_SERVICE_OK); + Require(value == setting.value); + } + UserServiceSetUserConnected_nid_postfix(1, false); Require(sceUserServiceGetRegisteredUserIdList(®istered) == USER_SERVICE_OK); Require(registered.user_id[0] == USER_SERVICE_INITIAL_USER_ID); for (int i = 1; i < 16; ++i) { Require(registered.user_id[i] == USER_SERVICE_USER_ID_INVALID); } + Require(sceUserServiceGetAgeLevel(USER_SERVICE_INITIAL_USER_ID + 1, &age_level) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + Require(sceUserServiceGetAgeLevel(USER_SERVICE_INITIAL_USER_ID + 1, nullptr) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + for (const Setting& setting : settings) { + std::int32_t value = 0x5a5a5a5a; + Require(setting.get(USER_SERVICE_INITIAL_USER_ID + 1, &value) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + Require(value == 0x5a5a5a5a); + Require(setting.get(USER_SERVICE_INITIAL_USER_ID + 1, nullptr) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + } } From 815fc413eac20cc21973d62920ed122b5499b5e5 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:56:28 +0200 Subject: [PATCH 314/428] feat(libSceFreeType): implement FT_Select_Size --- core/libs/prx/libSceFreeType/Export.cpp | 7 ++ core/libs/prx/libSceFreeType/include/Api.hpp | 1 + core/libs/tests/GuestFreeType.cpp | 97 ++++++++++++++++++++ docs/dev/debt/libSceFreeType.md | 2 + 4 files changed, 107 insertions(+) diff --git a/core/libs/prx/libSceFreeType/Export.cpp b/core/libs/prx/libSceFreeType/Export.cpp index 0ff5071b79..5c1e983f4e 100644 --- a/core/libs/prx/libSceFreeType/Export.cpp +++ b/core/libs/prx/libSceFreeType/Export.cpp @@ -114,6 +114,13 @@ int APS5_VABI FT_Done_FreeType_nid_postfix(GuestLibrary* library) { return error; } +int APS5_VABI FT_Select_Size_nid_postfix(GuestFaceRec* face, std::int32_t strikeIndex) { + GuestFace* owner = FaceOf(face); + const FT_Error error = FT_Select_Size(owner ? owner->host : nullptr, strikeIndex); + if (owner) SyncSize(*owner); + return error; +} + int APS5_VABI FT_New_Face_nid_postfix(GuestLibrary* library, const char* filepathname, std::int64_t face_index, GuestFaceRec** aface) { if (!filepathname) return FT_Err_Invalid_Argument; const std::int64_t faceIndex = OpenFaceIndex(face_index); diff --git a/core/libs/prx/libSceFreeType/include/Api.hpp b/core/libs/prx/libSceFreeType/include/Api.hpp index 7c2f791458..d29212fe8f 100644 --- a/core/libs/prx/libSceFreeType/include/Api.hpp +++ b/core/libs/prx/libSceFreeType/include/Api.hpp @@ -11,6 +11,7 @@ int APS5_VABI FT_Done_FreeType_nid_postfix(FreeType::GuestLibrary* library); int APS5_VABI FT_New_Face_nid_postfix(FreeType::GuestLibrary* library, const char* filepathname, std::int64_t faceIndex, FreeType::GuestFaceRec** aface); int APS5_VABI FT_New_Memory_Face_nid_postfix(FreeType::GuestLibrary* library, const unsigned char* fileBase, std::int64_t fileSize, std::int64_t faceIndex, FreeType::GuestFaceRec** aface); int APS5_VABI FT_Done_Face_nid_postfix(FreeType::GuestFaceRec* face); +int APS5_VABI FT_Select_Size_nid_postfix(FreeType::GuestFaceRec* face, std::int32_t strikeIndex); int APS5_VABI FT_Set_Pixel_Sizes_nid_postfix(FreeType::GuestFaceRec* face, std::uint32_t pixelWidth, std::uint32_t pixelHeight); int APS5_VABI FT_Set_Char_Size_nid_postfix(FreeType::GuestFaceRec* face, std::int64_t charWidth, std::int64_t charHeight, std::uint32_t horizontalResolution, std::uint32_t verticalResolution); std::uint32_t APS5_VABI FT_Get_Char_Index_nid_postfix(FreeType::GuestFaceRec* face, std::uint64_t charcode); diff --git a/core/libs/tests/GuestFreeType.cpp b/core/libs/tests/GuestFreeType.cpp index c14954f91c..c54607361a 100644 --- a/core/libs/tests/GuestFreeType.cpp +++ b/core/libs/tests/GuestFreeType.cpp @@ -660,6 +660,101 @@ static void APS5_VABI GuestFaceFinalizer(void* object) { static_cast(face->generic.data)->calls.push_back(3); } +static std::string BitmapStrikeFont(int width, int height, int descent) { + std::string font = "STARTFONT 2.1\nFONT -anyps5-strike-medium-r-normal--" + std::to_string(height) + "-0-72-72-c-0-iso10646-1\n"; + font += "SIZE " + std::to_string(height) + " 72 72\nFONTBOUNDINGBOX " + std::to_string(width) + " " + std::to_string(height) + " 0 -" + std::to_string(descent); + font += "\nSTARTPROPERTIES 4\nFONT_ASCENT " + std::to_string(height - descent) + "\nFONT_DESCENT " + std::to_string(descent); + font += "\nCHARSET_REGISTRY \"ISO10646\"\nCHARSET_ENCODING \"1\"\nENDPROPERTIES\nCHARS 1\nSTARTCHAR A\nENCODING 65\nSWIDTH 1000 0\nDWIDTH "; + font += std::to_string(width) + " 0\nBBX " + std::to_string(width) + " " + std::to_string(height) + " 0 -" + std::to_string(descent) + "\nBITMAP\n"; + for (int y = 0; y < height; ++y) { + for (int byte = 0; byte < (width + 7) / 8; ++byte) { + unsigned value = 0; + for (int bit = 0; bit < 8; ++bit) { + const int x = byte * 8 + bit; + if (x < width && (x == 0 || y == height - 1 || (x + y) % 3 == 0)) value |= 0x80u >> bit; + } + font += "0123456789ABCDEF"[value >> 4]; + font += "0123456789ABCDEF"[value & 15]; + } + font += '\n'; + } + return font + "ENDCHAR\nENDFONT\n"; +} + +static void CheckStrikeSelection(GuestLibrary* library, FT_Library nativeLibrary, GuestFaceRec* scalable, FT_Face nativeScalable) { + const auto sizeMatches = [](const GuestSizeRec* size, FT_Size host) { + const auto& actual = size->metrics; + const auto& expected = host->metrics; + Require(actual.x_ppem == expected.x_ppem && actual.y_ppem == expected.y_ppem); + Require(actual.x_scale == expected.x_scale && actual.y_scale == expected.y_scale); + Require(actual.ascender == expected.ascender && actual.descender == expected.descender); + Require(actual.height == expected.height && actual.max_advance == expected.max_advance); + }; + constexpr std::array invalidIndices{-1, 1, 2, std::numeric_limits::min(), std::numeric_limits::max()}; + int requests = 0; + int rendered = 0; + for (const auto index : invalidIndices) { + const auto nullError = FT_Select_Size(nullptr, index); + Require(nullError != 0 && FT_Select_Size_nid_postfix(nullptr, index) == nullError); + ++requests; + } + for (const auto index : {0, -1, std::numeric_limits::max()}) { + const auto error = FT_Select_Size(nativeScalable, index); + Require(error == FT_Err_Invalid_Face_Handle && FT_Select_Size_nid_postfix(scalable, index) == error); + sizeMatches(scalable->size, nativeScalable->size); + ++requests; + } + for (const auto& dimensions : {std::array{5, 7, 2}, std::array{11, 13, 3}}) { + const auto [width, height, descent] = dimensions; + const std::string font = BitmapStrikeFont(width, height, descent); + const auto* bytes = reinterpret_cast(font.data()); + GuestFaceRec* face = nullptr; + FT_Face native = nullptr; + Require(FT_New_Memory_Face_nid_postfix(library, bytes, static_cast(font.size()), 0, &face) == 0); + Require(FT_New_Memory_Face(nativeLibrary, bytes, static_cast(font.size()), 0, &native) == 0); + Require(FT_HAS_FIXED_SIZES(native) && !FT_IS_SCALABLE(native) && face->num_fixed_sizes == 1); + const GuestBitmapSize* strikes = face->available_sizes; + const auto& expectedStrike = native->available_sizes[0]; + Require(strikes[0].width == expectedStrike.width && strikes[0].height == expectedStrike.height && strikes[0].size == expectedStrike.size); + Require(strikes[0].x_ppem == expectedStrike.x_ppem && strikes[0].y_ppem == expectedStrike.y_ppem); + GuestSizeRec* size = face->size; + int marker = 0; + size->generic.data = ▮ + sizeMatches(size, native->size); + Require(size->metrics.y_ppem == 0); + const std::array indices{-1, 1, invalidIndices[3], invalidIndices[4], 0, 2, 0, -1, 0}; + for (const auto index : indices) { + face->glyph->advance.x = 0x123456789ABCll; + face->bbox.xMin = -0x123456789ABCll; + const auto error = FT_Select_Size(native, index); + Require((index == 0) == (error == 0)); + Require(FT_Select_Size_nid_postfix(face, index) == error); + ++requests; + sizeMatches(size, native->size); + Require(face->size == size && size->face == face && size->generic.data == &marker && face->available_sizes == strikes); + Require(face->glyph->advance.x == 0x123456789ABCll && face->bbox.xMin == -0x123456789ABCll); + if (error != 0) continue; + Require(size->metrics.y_ppem == height && size->metrics.ascender == (height - descent) * 64 && size->metrics.descender == -descent * 64); + for (const auto flags : std::array{FT_LOAD_DEFAULT, FT_LOAD_RENDER, FT_LOAD_RENDER | FT_LOAD_MONOCHROME}) { + Require(FT_Load_Char_nid_postfix(face, 'A', flags) == 0 && FT_Load_Char(native, 'A', flags) == 0); + SlotMatches(face->glyph, native->glyph); + const auto& bitmap = face->glyph->bitmap; + Require(face->glyph->format == FT_GLYPH_FORMAT_BITMAP && bitmap.pixel_mode == FT_PIXEL_MODE_MONO); + Require(bitmap.width == width && bitmap.rows == height && bitmap.pitch > 0 && face->glyph->bitmap_top == height - descent); + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + const bool pixel = (bitmap.buffer[y * bitmap.pitch + x / 8] & (0x80u >> (x % 8))) != 0; + Require(pixel == (x == 0 || y == height - 1 || (x + y) % 3 == 0)); + } + } + ++rendered; + } + } + Require(FT_Done_Face_nid_postfix(face) == 0 && FT_Done_Face(native) == 0); + } + std::printf("FT_Select_Size: %d native request comparisons, %d bitmap glyph comparisons\n", requests, rendered); +} + static void SetFinalizers(FT_Face face, Finalizers& events) { face->generic = {&events, NativeFaceFinalizer}; face->glyph->generic = {&events, NativeSlotFinalizer}; @@ -803,6 +898,8 @@ int main() { Require(FT_New_Face_nid_postfix(library, "/app0/font.ttf", 0, &face) == 0 && face != nullptr && face != masked); FaceMatches(face, host, library); + CheckStrikeSelection(library, hostLibrary, face, host); + const auto fontSize = static_cast(font.size()); GuestFaceRec* memoryFace = nullptr; Require(FT_New_Memory_Face_nid_postfix(library, nullptr, fontSize, 0, &memoryFace) == FT_Err_Invalid_Argument); diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 5fd44fd1d1..fbeba017ce 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -14,6 +14,8 @@ - [FT_Request_Size](../../../core/libs/prx/libSceFreeType/src/SizeRequest.cpp) (libSceFreeType) - console signature, request layout and errors are unverified; the guest request follows the LP64 public headers of the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/include/freetype/freetype.h). Host overflow behavior remains that of the bundled build. On Windows, native `FT_Long` is 32 bits; valid requests whose width or height exceeds that range throw before changing the size. +- [FT_Select_Size](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signatures and errors are unverified; the LP64 guest adapter follows the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Bitmap-strike selection and size metrics are checked against native FreeType with generated BDF fonts; other font drivers have not been exercised through this entry point. + - [FT_New_Memory_Face, FT_Done_Face and FT_Done_FreeType](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signatures, errors and generic-finalizer ordering are unverified; they follow the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/include/freetype/freetype.h) with LP64 guest arguments. Caller-owned font bytes must remain valid until the face is freed. - [FT_Outline_Check, FT_Outline_Get_CBox](../../../core/libs/prx/libSceFreeType/src/Outline.cpp) (libSceFreeType) - console signatures, count signedness and errors are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c), including its unsigned 16-bit counts and contour endpoints. Control-box coordinates use the existing guest LP64 layout and retain their full 64-bit range on Windows; contour checking does not validate point tags From a11ae425d446a09ef17f7580a9d3c52bf1d15e41 Mon Sep 17 00:00:00 2001 From: ElysiaAlarien <10030248+ElysiaAlarien@users.noreply.github.com> Date: Sat, 24 Oct 2026 21:38:57 -0400 Subject: [PATCH 315/428] docs(libSceMsgDialog): record the silent stub and parameter layout as debt Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceMsgDialog/Export.cpp | 13 +++---------- docs/dev/debt/libSceMsgDialog.md | 9 +++++++++ 2 files changed, 12 insertions(+), 10 deletions(-) create mode 100644 docs/dev/debt/libSceMsgDialog.md diff --git a/core/libs/prx/libSceMsgDialog/Export.cpp b/core/libs/prx/libSceMsgDialog/Export.cpp index a94de137f5..c9462f47b7 100644 --- a/core/libs/prx/libSceMsgDialog/Export.cpp +++ b/core/libs/prx/libSceMsgDialog/Export.cpp @@ -1,7 +1,4 @@ #include "prx/libSceMsgDialog/include/Api.hpp" -// 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 @@ -40,8 +37,8 @@ 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 kButtonIdOk = 1; +constexpr int kButtonIdNo = 2; std::mutex g_lock; int g_status = kStatusNone; @@ -63,8 +60,6 @@ bool Readable(const void* p, std::size_t n) { #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; @@ -94,7 +89,6 @@ int Poll() { 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; @@ -103,7 +97,7 @@ int Poll() { } return g_status; } -} // namespace +} extern "C" { @@ -123,7 +117,6 @@ int APS5_VABI sceMsgDialogOpen(const void* param) { 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(); diff --git a/docs/dev/debt/libSceMsgDialog.md b/docs/dev/debt/libSceMsgDialog.md new file mode 100644 index 0000000000..71acb51911 --- /dev/null +++ b/docs/dev/debt/libSceMsgDialog.md @@ -0,0 +1,9 @@ +# libSceMsgDialog technical debt + +### Silent stubs + +- [libSceMsgDialog](../../../core/libs/prx/libSceMsgDialog/Export.cpp) - message dialogs show no UI. `sceMsgDialogOpen` opens one without `sceMsgDialogInitialize` as well, and a user message or system message dialog finishes on the second `sceMsgDialogGetStatus` or `sceMsgDialogUpdateStatus` call; the first 160 characters of a user message are only logged. The button it picks (2 for `YES_NO_FOCUS_NO`, otherwise 1) is kept but not returned, as `sceMsgDialogGetResult` is exported by libSceMsgDialog.native. A progress bar dialog runs until `sceMsgDialogClose` or for 30 seconds, and `sceMsgDialogProgressBarInc`, `sceMsgDialogProgressBarSetMsg` and `sceMsgDialogProgressBarSetValue` ignore their arguments + +### Unknown function info + +- [sceMsgDialogOpen](../../../core/libs/prx/libSceMsgDialog/Export.cpp) (libSceMsgDialog) - the `SceMsgDialogParam` layout it reads is not measured on a PS5: the mode at 0x38 (1 user message, 2 progress bar, 3 system message), the user message parameter pointer at 0x40, and in that parameter the button type at 0 and the message pointer at 8. A mode outside 1 to 3, or a parameter that cannot be read, is treated as a user message; only Windows checks that the memory is readable, so on Linux any non-null pointer is read. The errors `0x80b80003` (not initialized), `0x80b80007` (busy), `0x80b8000b` (not running) and `0x80b8000d` (null argument) are not measured either. Closes when: measured on a PS5 From abd16e771d5d69fede9cde18d540b87c6e2738ff Mon Sep 17 00:00:00 2001 From: AZLisme <8706522+AZLisme@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:44:40 +0800 Subject: [PATCH 316/428] feat(libSceLibreSsl): implement ASN.1 integer and string helpers --- core/libs/prx/libSceLibreSsl/CMakeLists.txt | 10 +- core/libs/prx/libSceLibreSsl/Export.cpp | 25 -- core/libs/prx/libSceLibreSsl/src/Asn1.cpp | 133 +++++++++++ core/libs/prx/libSceLibreSsl/tests/Asn1.cpp | 247 ++++++++++++++++++++ docs/dev/debt/libSceLibreSsl.md | 6 +- 5 files changed, 394 insertions(+), 27 deletions(-) create mode 100644 core/libs/prx/libSceLibreSsl/src/Asn1.cpp create mode 100644 core/libs/prx/libSceLibreSsl/tests/Asn1.cpp diff --git a/core/libs/prx/libSceLibreSsl/CMakeLists.txt b/core/libs/prx/libSceLibreSsl/CMakeLists.txt index bd17c2379e..9612c0369f 100644 --- a/core/libs/prx/libSceLibreSsl/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl/CMakeLists.txt @@ -2,6 +2,7 @@ add_library(libSceLibreSsl SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/E target_include_directories(libSceLibreSsl PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceLibreSsl PRIVATE mbedcrypto) configure_windows_unwind(libSceLibreSsl) +target_sources(libSceLibreSsl PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/Asn1.cpp) if(BUILD_TESTING) add_test_executable(guest_libressl_x509_errors_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslX509Errors.cpp) @@ -21,8 +22,15 @@ if(BUILD_TESTING) configure_windows_unwind(guest_libressl_digest_tests) add_test(NAME guest_libressl_digest COMMAND guest_libressl_digest_tests) set_tests_properties(guest_libressl_digest PROPERTIES TIMEOUT 30) + add_test_executable(guest_libressl_asn1_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Asn1.cpp) + target_include_directories(guest_libressl_asn1_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_asn1_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_asn1_tests) + add_test(NAME guest_libressl_asn1 COMMAND guest_libressl_asn1_tests) + add_test(NAME guest_libressl_asn1_allocator COMMAND guest_libressl_asn1_tests allocator) + set_tests_properties(guest_libressl_asn1 guest_libressl_asn1_allocator PROPERTIES TIMEOUT 30) if(WIN32) - set_tests_properties(guest_libressl_digest PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_libressl_digest guest_libressl_asn1 guest_libressl_asn1_allocator PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 5cebe1b097..894b49bff5 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -19,36 +19,11 @@ constexpr std::size_t DesScheduleBytes = 128; extern "C" { APS5_DUMMY_FUN -int APS5_VABI ASN1_INTEGER_get_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ASN1_STRING_data_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ASN1_STRING_length_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI ASN1_STRING_print_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI ASN1_STRING_to_UTF8_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ASN1_STRING_type_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI ASN1_TIME_print_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl/src/Asn1.cpp b/core/libs/prx/libSceLibreSsl/src/Asn1.cpp new file mode 100644 index 0000000000..3c8b624d6a --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/Asn1.cpp @@ -0,0 +1,133 @@ +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" void* APS5_VABI malloc_nid_postfix(std::size_t size); + +namespace { + +struct Asn1String { + std::int32_t length; + std::int32_t type; + unsigned char* data; + std::int64_t flags; +}; + +static_assert(sizeof(Asn1String) == 24); +static_assert(offsetof(Asn1String, data) == 8); +static_assert(offsetof(Asn1String, flags) == 16); + +int CharacterWidth(int type) { + switch (type) { + case 12: return 0; + case 19: + case 20: + case 22: + case 23: + case 24: + case 26: return 1; + case 28: return 4; + case 30: return 2; + default: return -1; + } +} + +bool ReadCharacter(std::span& input, int width, std::uint32_t& value) { + value = 0; + if (width != 0) { + if (input.size() < static_cast(width)) return false; + for (int index = 0; index < width; ++index) value = (value << 8) | input[index]; + input = input.subspan(width); + } else { + const unsigned char lead = input.front(); + const int bytes = lead < 0x80 ? 1 : lead >= 0xc2 && lead <= 0xdf ? 2 + : lead >= 0xe0 && lead <= 0xef ? 3 : lead >= 0xf0 && lead <= 0xf4 ? 4 : 0; + if (bytes == 0 || input.size() < static_cast(bytes)) return false; + value = lead & (0x7f >> (bytes == 1 ? 0 : bytes)); + for (int index = 1; index < bytes; ++index) { + if ((input[index] & 0xc0) != 0x80) return false; + value = (value << 6) | (input[index] & 0x3f); + } + constexpr std::uint32_t minimum[] = {0, 0, 0x80, 0x800, 0x10000}; + if (value < minimum[bytes]) return false; + input = input.subspan(bytes); + } + return value <= 0x10ffff && (value < 0xd800 || value > 0xdfff); +} + +void AppendCharacter(std::vector& output, std::uint32_t value) { + const int bytes = value < 0x80 ? 1 : value < 0x800 ? 2 : value < 0x10000 ? 3 : 4; + if (output.size() >= static_cast(std::numeric_limits::max() - bytes)) + NotImplemented_nid_no_patch("ASN1_STRING_to_UTF8: output length overflow"); + if (bytes == 1) { + output.push_back(static_cast(value)); + return; + } + constexpr unsigned char prefix[] = {0, 0, 0xc0, 0xe0, 0xf0}; + output.push_back(static_cast(prefix[bytes] | (value >> (6 * (bytes - 1))))); + for (int index = bytes - 2; index >= 0; --index) + output.push_back(static_cast(0x80 | ((value >> (6 * index)) & 0x3f))); +} + +} // namespace + +extern "C" { + +std::int64_t APS5_VABI ASN1_INTEGER_get_nid_postfix(const void* value) { + if (!value) return 0; + const auto& integer = *static_cast(value); + if (integer.type != 2 && integer.type != 0x102) return -1; + if (integer.length > 8) return -1; + if (!integer.data) return 0; + std::uint64_t magnitude = 0; + for (int index = 0; index < integer.length; ++index) magnitude = (magnitude << 8) | integer.data[index]; + if (magnitude > static_cast(std::numeric_limits::max())) + NotImplemented_nid_no_patch(__func__); + const auto result = static_cast(magnitude); + return integer.type == 0x102 ? -result : result; +} + +unsigned char* APS5_VABI ASN1_STRING_data_nid_postfix(void* value) { + if (!value) NotImplemented_nid_no_patch(__func__); + return static_cast(value)->data; +} + +int APS5_VABI ASN1_STRING_length_nid_postfix(const void* value) { + if (!value) NotImplemented_nid_no_patch(__func__); + return static_cast(value)->length; +} + +int APS5_VABI ASN1_STRING_type_nid_postfix(void* value) { + if (!value) NotImplemented_nid_no_patch(__func__); + return static_cast(value)->type; +} + +int APS5_VABI ASN1_STRING_to_UTF8_nid_postfix(unsigned char** output, void* value) { + if (!value) return -1; + const auto& string = *static_cast(value); + const int width = CharacterWidth(string.type); + if (width < 0) return -1; + if (!output || (!string.data && string.length != 0)) NotImplemented_nid_no_patch(__func__); + std::size_t length = string.length < 0 ? std::strlen(reinterpret_cast(string.data)) + : static_cast(string.length); + if (length > static_cast(std::numeric_limits::max())) NotImplemented_nid_no_patch(__func__); + std::span input(string.data, length); + std::vector converted; + while (!input.empty()) { + std::uint32_t character; + if (!ReadCharacter(input, width, character)) NotImplemented_nid_no_patch(__func__); + AppendCharacter(converted, character); + } + auto* result = static_cast(malloc_nid_postfix(converted.size() + 1)); + if (!result) NotImplemented_nid_no_patch(__func__); + if (!converted.empty()) std::memcpy(result, converted.data(), converted.size()); + result[converted.size()] = 0; + *output = result; + return static_cast(converted.size()); +} + +} diff --git a/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp b/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp new file mode 100644 index 0000000000..bab725d606 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp @@ -0,0 +1,247 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +std::int64_t APS5_VABI ASN1_INTEGER_get_nid_postfix(const void* value); +unsigned char* APS5_VABI ASN1_STRING_data_nid_postfix(void* value); +int APS5_VABI ASN1_STRING_length_nid_postfix(const void* value); +int APS5_VABI ASN1_STRING_type_nid_postfix(void* value); +int APS5_VABI ASN1_STRING_to_UTF8_nid_postfix(unsigned char** output, void* value); +void APS5_VABI free_nid_postfix(void* pointer); +} + +namespace { + +struct String { + std::int32_t length; + std::int32_t type; + unsigned char* data; + std::int64_t flags; +}; + +static_assert(sizeof(String) == 24); +static_assert(offsetof(String, data) == 8); +static_assert(offsetof(String, flags) == 16); + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "failed: %s\n", message); + std::abort(); +} + +template +void RequireThrows(TAction action, const char* message) { + try { + action(); + } catch (const std::exception&) { + return; + } + Require(false, message); +} + +void CheckConversion(int type, std::span input, std::span expected, int length = -2) { + std::vector storage(input.begin(), input.end()); + storage.push_back(0); + String value{length == -2 ? static_cast(input.size()) : length, type, storage.data(), 0x123456789abcdef}; + const String original = value; + unsigned char sentinel = 0xab; + unsigned char* result = &sentinel; + Require(ASN1_STRING_to_UTF8_nid_postfix(&result, &value) == static_cast(expected.size()), "UTF-8 byte length"); + Require(result && result != &sentinel && result != storage.data(), "conversion owns a new buffer"); + Require(expected.empty() || std::memcmp(result, expected.data(), expected.size()) == 0, "converted UTF-8 bytes"); + Require(result[expected.size()] == 0, "conversion terminator"); + Require(value.length == original.length && value.type == original.type && value.data == original.data + && value.flags == original.flags, "conversion preserves the input object"); + Require(std::equal(input.begin(), input.end(), storage.begin()), "conversion preserves the input bytes"); + free_nid_postfix(result); +} + +void CheckInvalidEncoding(int type, std::span input) { + String value{static_cast(input.size()), type, const_cast(input.data()), 0}; + unsigned char sentinel = 0xab; + unsigned char* result = &sentinel; + RequireThrows([&] { ASN1_STRING_to_UTF8_nid_postfix(&result, &value); }, "an unimplemented error-queue path throws"); + Require(result == &sentinel && sentinel == 0xab, "failure preserves the output pointer"); +} + +void Accessors() { + unsigned char data[] = {'a', 0, 'b'}; + String value{3, 12, data, std::numeric_limits::max()}; + Require(ASN1_STRING_data_nid_postfix(&value) == data, "data accessor preserves pointer identity"); + ASN1_STRING_data_nid_postfix(&value)[2] = 'c'; + Require(data[2] == 'c', "data accessor permits writes"); + Require(ASN1_STRING_length_nid_postfix(&value) == 3, "length includes embedded NUL"); + Require(ASN1_STRING_type_nid_postfix(&value) == 12, "type accessor"); + value = {-9, 0x7fffffff, nullptr, -1}; + Require(ASN1_STRING_data_nid_postfix(&value) == nullptr, "null data is preserved"); + Require(ASN1_STRING_length_nid_postfix(&value) == -9, "length is returned without normalization"); + Require(ASN1_STRING_type_nid_postfix(&value) == 0x7fffffff, "type is returned without normalization"); + RequireThrows([] { ASN1_STRING_data_nid_postfix(nullptr); }, "null data accessor input"); + RequireThrows([] { ASN1_STRING_length_nid_postfix(nullptr); }, "null length accessor input"); + RequireThrows([] { ASN1_STRING_type_nid_postfix(nullptr); }, "null type accessor input"); +} + +void Integers() { + Require(ASN1_INTEGER_get_nid_postfix(nullptr) == 0, "null integer"); + unsigned char data[] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0}; + constexpr std::array expected{ + 0, 0x01, 0x0123, 0x012345, 0x01234567, 0x0123456789, 0x0123456789ab, + 0x0123456789abcd, 0x0123456789abcdef}; + for (int length = 0; length <= 8; ++length) { + String value{length, 2, data, -1}; + Require(ASN1_INTEGER_get_nid_postfix(&value) == expected[length], "positive integer and 64-bit guest long"); + value.type = 0x102; + Require(ASN1_INTEGER_get_nid_postfix(&value) == -expected[length], "negative integer magnitude"); + } + String value{9, 2, nullptr, 0}; + Require(ASN1_INTEGER_get_nid_postfix(&value) == -1, "overlength is rejected before null data"); + value.length = 8; + Require(ASN1_INTEGER_get_nid_postfix(&value) == 0, "integer with null data"); + value = {-1, 2, data, 0}; + Require(ASN1_INTEGER_get_nid_postfix(&value) == 0, "negative integer length"); + for (int type : {-1, 0, 1, 3, 12, 0x100, 0x202, 0x302}) { + value.type = type; + Require(ASN1_INTEGER_get_nid_postfix(&value) == -1, "only integer types are accepted"); + } + std::array maximum{0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; + value = {8, 2, maximum.data(), 0}; + Require(ASN1_INTEGER_get_nid_postfix(&value) == std::numeric_limits::max(), "maximum signed integer"); + value.type = 0x102; + Require(ASN1_INTEGER_get_nid_postfix(&value) == -std::numeric_limits::max(), "negative maximum magnitude"); + maximum[0] = 0x80; + for (int type : {2, 0x102}) { + value.type = type; + RequireThrows([&] { ASN1_INTEGER_get_nid_postfix(&value); }, "source signed-overflow path throws"); + } + std::array zero{}; + value = {9, 2, zero.data(), 0}; + Require(ASN1_INTEGER_get_nid_postfix(&value) == -1, "overlength leading zeros are not trimmed"); +} + +void Strings() { + constexpr unsigned char latin[] = {0, 0x41, 0x7f, 0x80, 0xa3, 0xff}; + constexpr unsigned char latinUtf8[] = {0, 0x41, 0x7f, 0xc2, 0x80, 0xc2, 0xa3, 0xc3, 0xbf}; + constexpr unsigned char bmp[] = {0, 0, 0, 0x7f, 0, 0x80, 7, 0xff, 8, 0, 0x20, 0xac, 0xd7, 0xff, 0xe0, 0, 0xff, 0xff}; + constexpr unsigned char bmpUtf8[] = {0, 0x7f, 0xc2, 0x80, 0xdf, 0xbf, 0xe0, 0xa0, 0x80, 0xe2, 0x82, 0xac, + 0xed, 0x9f, 0xbf, 0xee, 0x80, 0x80, 0xef, 0xbf, 0xbf}; + constexpr unsigned char universal[] = {0, 0, 0, 0x41, 0, 1, 0, 0, 0, 1, 0xf6, 0, 0, 0x10, 0xff, 0xff}; + constexpr unsigned char universalUtf8[] = {0x41, 0xf0, 0x90, 0x80, 0x80, 0xf0, 0x9f, 0x98, 0x80, 0xf4, 0x8f, 0xbf, 0xbf}; + for (int type : {19, 20, 22, 23, 24, 26}) CheckConversion(type, latin, latinUtf8); + CheckConversion(30, bmp, bmpUtf8); + CheckConversion(28, universal, universalUtf8); + CheckConversion(12, bmpUtf8, bmpUtf8); + CheckConversion(12, universalUtf8, universalUtf8); + for (int type : {12, 19, 20, 22, 23, 24, 26, 28, 30}) { + CheckConversion(type, {}, {}); + String empty{0, type, nullptr, 0}; + unsigned char* result = nullptr; + Require(ASN1_STRING_to_UTF8_nid_postfix(&result, &empty) == 0 && result && *result == 0, "empty null-data string"); + free_nid_postfix(result); + } + constexpr unsigned char terminated[] = {'A', 0xc2, 0xa3, 0, 'X'}; + CheckConversion(12, terminated, std::span(terminated).first(3), -1); + CheckConversion(12, terminated, std::span(terminated).first(3), -7); + constexpr unsigned char ascii[] = {'A', 'B', 'C'}; + CheckConversion(19, ascii, ascii, -1); + + unsigned char sentinel = 0xab; + unsigned char* result = &sentinel; + Require(ASN1_STRING_to_UTF8_nid_postfix(&result, nullptr) == -1 && result == &sentinel, "null conversion input"); + Require(ASN1_STRING_to_UTF8_nid_postfix(nullptr, nullptr) == -1, "null input checked before output"); + for (int type = -1; type <= 32; ++type) { + if (type == 12 || type == 19 || type == 20 || type == 22 || type == 23 || type == 24 || type == 26 || type == 28 || type == 30) continue; + String value{0, type, nullptr, 0}; + Require(ASN1_STRING_to_UTF8_nid_postfix(&result, &value) == -1 && result == &sentinel, "unsupported string tag"); + Require(ASN1_STRING_to_UTF8_nid_postfix(nullptr, &value) == -1, "tag checked before output"); + } + String value{0, 12, nullptr, 0}; + RequireThrows([&] { ASN1_STRING_to_UTF8_nid_postfix(nullptr, &value); }, "null conversion output"); + value.length = 1; + RequireThrows([&] { ASN1_STRING_to_UTF8_nid_postfix(&result, &value); }, "null nonempty string data"); + value.length = -1; + RequireThrows([&] { ASN1_STRING_to_UTF8_nid_postfix(&result, &value); }, "null terminated-string data"); +} + +void InvalidEncodings() { + const std::vector> utf8{ + {0x80}, {0xbf}, {0xc0, 0x80}, {0xc1, 0xbf}, {0xc2}, {0xc2, 0x41}, {0xe0, 0x80, 0x80}, + {0xe1, 0x80}, {0xe1, 0x41, 0x80}, {0xe1, 0x80, 0x41}, {0xed, 0xa0, 0x80}, {0xed, 0xbf, 0xbf}, + {0xf0, 0x80, 0x80, 0x80}, {0xf0, 0x90, 0x80}, {0xf0, 0x90, 0x80, 0x41}, {0xf4, 0x90, 0x80, 0x80}, + {0xf5, 0x80, 0x80, 0x80}, {0xf8, 0x88, 0x80, 0x80, 0x80}, {0xfe}, {0xff}, {'a', 0xff}}; + for (const auto& bytes : utf8) CheckInvalidEncoding(12, bytes); + const std::vector> bmp{{0}, {0, 'A', 0}, {0xd8, 0}, {0xdf, 0xff}, {0xd8, 0, 0xdc, 0}}; + for (const auto& bytes : bmp) CheckInvalidEncoding(30, bytes); + const std::vector> universal{ + {0}, {0, 0}, {0, 0, 0}, {0, 0, 0xd8, 0}, {0, 0x11, 0, 0}, {0xff, 0xff, 0xff, 0xff}}; + for (const auto& bytes : universal) CheckInvalidEncoding(28, bytes); +} + +std::size_t lastAllocationSize = 0; +int allocationCount = 0; +int freeCount = 0; +bool failAllocation = false; + +void* APS5_VABI Allocate_nid_no_patch(std::size_t size) { + lastAllocationSize = size; + ++allocationCount; + return failAllocation ? nullptr : std::malloc(size); +} + +void APS5_VABI Free_nid_no_patch(void* pointer) { + ++freeCount; + std::free(pointer); +} + +void* APS5_VABI Calloc_nid_no_patch(std::size_t, std::size_t) { throw std::runtime_error("unexpected calloc"); } +void* APS5_VABI Reallocate_nid_no_patch(void*, std::size_t) { throw std::runtime_error("unexpected realloc"); } +void* APS5_VABI Align_nid_no_patch(std::size_t, std::size_t) { throw std::runtime_error("unexpected aligned allocation"); } +void* APS5_VABI Realign_nid_no_patch(void*, std::size_t, std::size_t) { throw std::runtime_error("unexpected aligned reallocation"); } +int APS5_VABI PosixAlign_nid_no_patch(void**, std::size_t, std::size_t) { throw std::runtime_error("unexpected posix_memalign"); } + +void Allocator() { + std::array api{ + reinterpret_cast(Allocate_nid_no_patch), reinterpret_cast(Free_nid_no_patch), reinterpret_cast(Calloc_nid_no_patch), + reinterpret_cast(Reallocate_nid_no_patch), reinterpret_cast(Align_nid_no_patch), reinterpret_cast(Realign_nid_no_patch), + reinterpret_cast(PosixAlign_nid_no_patch)}; + ApplicationHeapRegister_nid_no_patch(api.data()); + constexpr unsigned char input[] = {0xff}; + constexpr unsigned char expected[] = {0xc3, 0xbf}; + CheckConversion(20, input, expected); + Require(allocationCount == 1 && lastAllocationSize == 3 && freeCount == 1, "conversion uses the application allocator"); + failAllocation = true; + String value{1, 20, const_cast(input), 0}; + unsigned char sentinel = 0xab; + unsigned char* result = &sentinel; + RequireThrows([&] { ASN1_STRING_to_UTF8_nid_postfix(&result, &value); }, "allocation failure throws"); + Require(allocationCount == 2 && freeCount == 1 && result == &sentinel && sentinel == 0xab, "allocation failure preserves ownership"); + constexpr unsigned char invalid[] = {0xff}; + CheckInvalidEncoding(12, invalid); + Require(allocationCount == 2, "malformed input is checked before allocation"); +} + +} // namespace + +int main(int argc, char**) { + if (argc > 1) { + Allocator(); + return 0; + } + std::array api{}; + ApplicationHeapRegister_nid_no_patch(api.data()); + Accessors(); + Integers(); + Strings(); + InvalidEncodings(); +} diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index c562a24a2a..67ab504fea 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -2,10 +2,14 @@ ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 353 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 348 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. - [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); the module's LibreSSL version is unknown and nothing is checked on the console. Behaviour that differs between LibreSSL versions throws: `SHA256` with a null digest buffer (older versions return a static buffer) and `SHA512_Final` with the SHA-512/224 or SHA-512/256 digest length. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw - [DES_set_key, DES_ecb_encrypt, DES_set_odd_parity](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the 128-byte `DES_key_schedule` holds the 8-byte key followed by zeros instead of LibreSSL's precomputed subkeys, so a title reading its contents sees different bytes; `DES_ecb_encrypt` derives the subkeys through mbedtls on each call. Not compared with the console. Results follow LibreSSL [des_key.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des_key.c) and [des.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des.c): `DES_set_key` skips the parity and weak-key checks and returns 0, as it does while `DES_check_key` is 0 (that variable is not in the PS5 export inventory), a nonzero `enc` encrypts, and `DES_set_odd_parity` sets each byte's low bit for odd parity. Null pointers throw where LibreSSL faults - [X509_verify_cert_error_string](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - no console call is verified. The 64-bit argument, low-32-bit lookup, messages and shared numeric fallback follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509/x509_txt.c), the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware versions remain unverified. Unknown-code results share an overwritten static buffer and are not safe for concurrent use, as in that release; code 1 and codes above 54 use the numeric fallback. - [RAND_add, RAND_bytes, RAND_status, RAND_load_file, RAND_file_name, CRYPTO_malloc, CRYPTO_free, CRYPTO_set_mem_functions](../../../core/libs/prx/libSceLibreSsl/src/Runtime.cpp) (libSceLibreSsl) - follow LibreSSL's [rand_lib.c, randfile.c](https://github.com/libressl/openbsd/tree/0f0b16dc977f6ca499f0f4540078d992c96da597/src/lib/libcrypto/rand) and [malloc-wrapper.c](https://github.com/libressl/openbsd/blob/0f0b16dc977f6ca499f0f4540078d992c96da597/src/lib/libcrypto/malloc-wrapper.c), which are the same in 2.6.5 (3.4.3 linked); nothing is checked on the console. `RAND_bytes` reads host entropy and `CRYPTO_malloc` and `CRYPTO_free` use the guest heap. `CRYPTO_malloc` returns null for a size of zero or less, as LibreSSL 2.5.4 to 3.4 do; later versions take a `size_t` size and call `malloc`. A null buffer with a positive length throws - [AES_set_encrypt_key, AES_set_decrypt_key, AES_encrypt, AES_decrypt, AES_ecb_encrypt, AES_cbc_encrypt, AES_cfb128_encrypt, AES_ctr128_encrypt](../../../core/libs/prx/libSceLibreSsl/src/Aes.cpp) (libSceLibreSsl) - follow LibreSSL's [aes.h](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/aes/aes.h) and [mode code](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/modes). Nothing is checked on the console. Key setup follows [LibreSSL 2.5.4's aes_core.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/aes/aes_core.c), the version the `X509_verify_cert_error_string` entry names: a null pointer returns -1 and a key length other than 128, 192 or 256 bits returns -2 before the key is written, and only the 44, 52 or 60 words the key length needs are written. The `AES_KEY` schedule uses that file's generic C layout (big-endian words, `rounds` 10, 12 or 14). The 2.5.4 amd64 build uses [aes-x86_64](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/aes/asm/aes-x86_64.pl) instead, which copies the key bytes without swapping, so each word is byte-reversed compared with the C layout (OpenSSL 3.0.13's x86-64 code gives the same schedules byte-reversed); the console's build options are unknown, so a schedule the title reads or fills is unverified. CTR leaves the last keystream block in `ecount_buf` even at a block boundary. Null pointers in the cipher and mode functions, other `rounds` values and a CFB or CTR position outside 0-15 throw - [ERR_get_error, ERR_peek_error, ERR_clear_error, ERR_remove_thread_state](../../../core/libs/prx/libSceLibreSsl/src/Error.cpp) (libSceLibreSsl) - no export of this library adds an error yet, so the queue stays empty for guest code. The per-thread queue of 16 slots holding 15 codes, its order and the `ERR_PACK` layout follow LibreSSL's [err.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/err/err.c) and [err.h](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/err/err.h); the module's LibreSSL version is unknown and nothing is checked on the console. The queue keeps no file, line or data, as no export reads them. `ERR_remove_thread_state` with a thread id throws: that version asserts a null id, while older versions remove the state of the thread the id names + +### Functional + +- [ASN1_INTEGER_get and ASN1_STRING_data/length/type/to_UTF8](../../../core/libs/prx/libSceLibreSsl/src/Asn1.cpp) are unverified on PS5. Signatures, the 24-byte guest string layout and conversions follow [LibreSSL 2.5.4's ASN.1 sources](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/asn1), the version reported for firmware 13.40 in [discussion #8981](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Null accessor inputs, null conversion outputs, missing nonempty data, oversized converted output and integer magnitudes exceeding INT64_MAX throw where the source has undefined behaviour. Malformed encodings and allocation failure throw until ASN.1 error-queue reporting is implemented. Conversion buffers use the guest libc allocator and can be released with guest `free`. From caa81ac8e79de176f3787b675e54b80c257857c1 Mon Sep 17 00:00:00 2001 From: AZLisme <8706522+AZLisme@users.noreply.github.com> Date: Fri, 23 Oct 2026 12:38:34 +0800 Subject: [PATCH 317/428] test(libSceLibreSsl): pin legacy integer length behavior --- core/libs/prx/libSceLibreSsl/src/Asn1.cpp | 2 +- core/libs/prx/libSceLibreSsl/tests/Asn1.cpp | 12 +++++++++++- docs/dev/debt/libSceLibreSsl.md | 2 ++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceLibreSsl/src/Asn1.cpp b/core/libs/prx/libSceLibreSsl/src/Asn1.cpp index 3c8b624d6a..f5b27f32a2 100644 --- a/core/libs/prx/libSceLibreSsl/src/Asn1.cpp +++ b/core/libs/prx/libSceLibreSsl/src/Asn1.cpp @@ -82,7 +82,7 @@ std::int64_t APS5_VABI ASN1_INTEGER_get_nid_postfix(const void* value) { const auto& integer = *static_cast(value); if (integer.type != 2 && integer.type != 0x102) return -1; if (integer.length > 8) return -1; - if (!integer.data) return 0; + if (integer.length <= 0 || !integer.data) return 0; std::uint64_t magnitude = 0; for (int index = 0; index < integer.length; ++index) magnitude = (magnitude << 8) | integer.data[index]; if (magnitude > static_cast(std::numeric_limits::max())) diff --git a/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp b/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp index bab725d606..cb709ab34f 100644 --- a/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp +++ b/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp @@ -109,8 +109,18 @@ void Integers() { Require(ASN1_INTEGER_get_nid_postfix(&value) == -1, "overlength is rejected before null data"); value.length = 8; Require(ASN1_INTEGER_get_nid_postfix(&value) == 0, "integer with null data"); + for (int length : {std::numeric_limits::min(), -8, -1, 0}) { + for (int type : {2, 0x102}) { + for (auto* bytes : {data, static_cast(nullptr), reinterpret_cast(1)}) { + value = {length, type, bytes, -1}; + const String original = value; + Require(ASN1_INTEGER_get_nid_postfix(&value) == 0, "LibreSSL 2.5.4 nonpositive integer length"); + Require(value.length == original.length && value.type == original.type && value.data == original.data + && value.flags == original.flags, "nonpositive integer length preserves input"); + } + } + } value = {-1, 2, data, 0}; - Require(ASN1_INTEGER_get_nid_postfix(&value) == 0, "negative integer length"); for (int type : {-1, 0, 1, 3, 12, 0x100, 0x202, 0x302}) { value.type = type; Require(ASN1_INTEGER_get_nid_postfix(&value) == -1, "only integer types are accepted"); diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index 67ab504fea..8ab9edf7ad 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -12,4 +12,6 @@ ### Functional +- [ASN1_INTEGER_get](../../../core/libs/prx/libSceLibreSsl/src/Asn1.cpp) has no console verification for nonpositive lengths. It follows [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/asn1/a_int.c#L363-L389): valid integer types return zero without reading data when length is zero or negative; invalid types return -1 first. Later LibreSSL versions reject negative lengths through `ASN1_INTEGER_get_int64`; that later contract is not used for this library. + - [ASN1_INTEGER_get and ASN1_STRING_data/length/type/to_UTF8](../../../core/libs/prx/libSceLibreSsl/src/Asn1.cpp) are unverified on PS5. Signatures, the 24-byte guest string layout and conversions follow [LibreSSL 2.5.4's ASN.1 sources](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/asn1), the version reported for firmware 13.40 in [discussion #8981](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Null accessor inputs, null conversion outputs, missing nonempty data, oversized converted output and integer magnitudes exceeding INT64_MAX throw where the source has undefined behaviour. Malformed encodings and allocation failure throw until ASN.1 error-queue reporting is implemented. Conversion buffers use the guest libc allocator and can be released with guest `free`. From 2174031ae5aafe6b3dd0ced28e803153a7fddae5 Mon Sep 17 00:00:00 2001 From: AZLisme <8706522+AZLisme@users.noreply.github.com> Date: Fri, 23 Oct 2026 18:42:16 +0800 Subject: [PATCH 318/428] test(libSceLibreSsl): avoid pinning unfinished error reporting --- core/libs/prx/libSceLibreSsl/tests/Asn1.cpp | 35 +-------------------- 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp b/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp index cb709ab34f..463f7f13ac 100644 --- a/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp +++ b/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp @@ -68,14 +68,6 @@ void CheckConversion(int type, std::span input, std::span input) { - String value{static_cast(input.size()), type, const_cast(input.data()), 0}; - unsigned char sentinel = 0xab; - unsigned char* result = &sentinel; - RequireThrows([&] { ASN1_STRING_to_UTF8_nid_postfix(&result, &value); }, "an unimplemented error-queue path throws"); - Require(result == &sentinel && sentinel == 0xab, "failure preserves the output pointer"); -} - void Accessors() { unsigned char data[] = {'a', 0, 'b'}; String value{3, 12, data, std::numeric_limits::max()}; @@ -184,29 +176,14 @@ void Strings() { RequireThrows([&] { ASN1_STRING_to_UTF8_nid_postfix(&result, &value); }, "null terminated-string data"); } -void InvalidEncodings() { - const std::vector> utf8{ - {0x80}, {0xbf}, {0xc0, 0x80}, {0xc1, 0xbf}, {0xc2}, {0xc2, 0x41}, {0xe0, 0x80, 0x80}, - {0xe1, 0x80}, {0xe1, 0x41, 0x80}, {0xe1, 0x80, 0x41}, {0xed, 0xa0, 0x80}, {0xed, 0xbf, 0xbf}, - {0xf0, 0x80, 0x80, 0x80}, {0xf0, 0x90, 0x80}, {0xf0, 0x90, 0x80, 0x41}, {0xf4, 0x90, 0x80, 0x80}, - {0xf5, 0x80, 0x80, 0x80}, {0xf8, 0x88, 0x80, 0x80, 0x80}, {0xfe}, {0xff}, {'a', 0xff}}; - for (const auto& bytes : utf8) CheckInvalidEncoding(12, bytes); - const std::vector> bmp{{0}, {0, 'A', 0}, {0xd8, 0}, {0xdf, 0xff}, {0xd8, 0, 0xdc, 0}}; - for (const auto& bytes : bmp) CheckInvalidEncoding(30, bytes); - const std::vector> universal{ - {0}, {0, 0}, {0, 0, 0}, {0, 0, 0xd8, 0}, {0, 0x11, 0, 0}, {0xff, 0xff, 0xff, 0xff}}; - for (const auto& bytes : universal) CheckInvalidEncoding(28, bytes); -} - std::size_t lastAllocationSize = 0; int allocationCount = 0; int freeCount = 0; -bool failAllocation = false; void* APS5_VABI Allocate_nid_no_patch(std::size_t size) { lastAllocationSize = size; ++allocationCount; - return failAllocation ? nullptr : std::malloc(size); + return std::malloc(size); } void APS5_VABI Free_nid_no_patch(void* pointer) { @@ -230,15 +207,6 @@ void Allocator() { constexpr unsigned char expected[] = {0xc3, 0xbf}; CheckConversion(20, input, expected); Require(allocationCount == 1 && lastAllocationSize == 3 && freeCount == 1, "conversion uses the application allocator"); - failAllocation = true; - String value{1, 20, const_cast(input), 0}; - unsigned char sentinel = 0xab; - unsigned char* result = &sentinel; - RequireThrows([&] { ASN1_STRING_to_UTF8_nid_postfix(&result, &value); }, "allocation failure throws"); - Require(allocationCount == 2 && freeCount == 1 && result == &sentinel && sentinel == 0xab, "allocation failure preserves ownership"); - constexpr unsigned char invalid[] = {0xff}; - CheckInvalidEncoding(12, invalid); - Require(allocationCount == 2, "malformed input is checked before allocation"); } } // namespace @@ -253,5 +221,4 @@ int main(int argc, char**) { Accessors(); Integers(); Strings(); - InvalidEncodings(); } From 931efc838cfa1ee92a4cacf287710df30cc57eb3 Mon Sep 17 00:00:00 2001 From: AZLisme <8706522+AZLisme@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:07:04 +0800 Subject: [PATCH 319/428] fix(libSceLibreSsl): handle minimum signed ASN.1 integer --- core/libs/prx/libSceLibreSsl/src/Asn1.cpp | 8 +++++--- core/libs/prx/libSceLibreSsl/tests/Asn1.cpp | 12 ++++++++++++ docs/dev/debt/libSceLibreSsl.md | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceLibreSsl/src/Asn1.cpp b/core/libs/prx/libSceLibreSsl/src/Asn1.cpp index f5b27f32a2..d83138ff0f 100644 --- a/core/libs/prx/libSceLibreSsl/src/Asn1.cpp +++ b/core/libs/prx/libSceLibreSsl/src/Asn1.cpp @@ -85,10 +85,12 @@ std::int64_t APS5_VABI ASN1_INTEGER_get_nid_postfix(const void* value) { if (integer.length <= 0 || !integer.data) return 0; std::uint64_t magnitude = 0; for (int index = 0; index < integer.length; ++index) magnitude = (magnitude << 8) | integer.data[index]; - if (magnitude > static_cast(std::numeric_limits::max())) + const bool negative = integer.type == 0x102; + const auto limit = static_cast(std::numeric_limits::max()) + negative; + if (magnitude > limit) NotImplemented_nid_no_patch(__func__); - const auto result = static_cast(magnitude); - return integer.type == 0x102 ? -result : result; + if (negative && magnitude != 0) return -static_cast(magnitude - 1) - 1; + return static_cast(magnitude); } unsigned char* APS5_VABI ASN1_STRING_data_nid_postfix(void* value) { diff --git a/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp b/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp index 463f7f13ac..6ded0680c6 100644 --- a/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp +++ b/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp @@ -122,6 +122,18 @@ void Integers() { Require(ASN1_INTEGER_get_nid_postfix(&value) == std::numeric_limits::max(), "maximum signed integer"); value.type = 0x102; Require(ASN1_INTEGER_get_nid_postfix(&value) == -std::numeric_limits::max(), "negative maximum magnitude"); + std::array minimum{0x80, 0, 0, 0, 0, 0, 0, 0}; + value = {8, 0x102, minimum.data(), -1}; + const String original = value; + Require(ASN1_INTEGER_get_nid_postfix(&value) == std::numeric_limits::min(), "minimum signed integer"); + Require(value.length == original.length && value.type == original.type && value.data == original.data + && value.flags == original.flags && minimum == std::array{0x80, 0, 0, 0, 0, 0, 0, 0}, "minimum integer preserves input"); + value.type = 2; + RequireThrows([&] { ASN1_INTEGER_get_nid_postfix(&value); }, "positive magnitude above signed maximum"); + value.type = 0x102; + minimum.back() = 1; + RequireThrows([&] { ASN1_INTEGER_get_nid_postfix(&value); }, "negative magnitude below signed minimum"); + value.data = maximum.data(); maximum[0] = 0x80; for (int type : {2, 0x102}) { value.type = type; diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index 8ab9edf7ad..bbc0e9fb0e 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -14,4 +14,4 @@ - [ASN1_INTEGER_get](../../../core/libs/prx/libSceLibreSsl/src/Asn1.cpp) has no console verification for nonpositive lengths. It follows [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/asn1/a_int.c#L363-L389): valid integer types return zero without reading data when length is zero or negative; invalid types return -1 first. Later LibreSSL versions reject negative lengths through `ASN1_INTEGER_get_int64`; that later contract is not used for this library. -- [ASN1_INTEGER_get and ASN1_STRING_data/length/type/to_UTF8](../../../core/libs/prx/libSceLibreSsl/src/Asn1.cpp) are unverified on PS5. Signatures, the 24-byte guest string layout and conversions follow [LibreSSL 2.5.4's ASN.1 sources](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/asn1), the version reported for firmware 13.40 in [discussion #8981](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Null accessor inputs, null conversion outputs, missing nonempty data, oversized converted output and integer magnitudes exceeding INT64_MAX throw where the source has undefined behaviour. Malformed encodings and allocation failure throw until ASN.1 error-queue reporting is implemented. Conversion buffers use the guest libc allocator and can be released with guest `free`. +- [ASN1_INTEGER_get and ASN1_STRING_data/length/type/to_UTF8](../../../core/libs/prx/libSceLibreSsl/src/Asn1.cpp) are unverified on PS5. Signatures, the 24-byte guest string layout and conversions follow [LibreSSL 2.5.4's ASN.1 sources](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/asn1), the version reported for firmware 13.40 in [discussion #8981](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Null accessor inputs, null conversion outputs, missing nonempty data, oversized converted output and unrepresentable signed integer values throw where the source has undefined behaviour. Malformed encodings and allocation failure throw until ASN.1 error-queue reporting is implemented. Conversion buffers use the guest libc allocator and can be released with guest `free`. From 6098cf2e6463f0a1726c121b5e3616d450c38246 Mon Sep 17 00:00:00 2001 From: AZLisme <8706522+AZLisme@users.noreply.github.com> Date: Sun, 25 Oct 2026 08:40:20 +0800 Subject: [PATCH 320/428] fix(libSceLibreSsl): share public ASN.1 declarations --- core/libs/prx/libSceLibreSsl/include/Api.hpp | 17 +++++--- core/libs/prx/libSceLibreSsl/src/Asn1.cpp | 31 ++++++-------- core/libs/prx/libSceLibreSsl/tests/Asn1.cpp | 45 +++++++------------- 3 files changed, 39 insertions(+), 54 deletions(-) diff --git a/core/libs/prx/libSceLibreSsl/include/Api.hpp b/core/libs/prx/libSceLibreSsl/include/Api.hpp index b610257cb7..b23740b20a 100644 --- a/core/libs/prx/libSceLibreSsl/include/Api.hpp +++ b/core/libs/prx/libSceLibreSsl/include/Api.hpp @@ -2,6 +2,13 @@ #include "SceTypes.hpp" +struct Asn1String { + std::int32_t length; + std::int32_t type; + unsigned char* data; + std::int64_t flags; +}; + inline constexpr int MaxRounds = 14; struct AesKey { @@ -46,12 +53,12 @@ struct Sha512Context { extern "C" { -int APS5_VABI ASN1_INTEGER_get_nid_postfix(void); -int APS5_VABI ASN1_STRING_data_nid_postfix(void); -int APS5_VABI ASN1_STRING_length_nid_postfix(void); +std::int64_t APS5_VABI ASN1_INTEGER_get_nid_postfix(const Asn1String* value); +unsigned char* APS5_VABI ASN1_STRING_data_nid_postfix(Asn1String* value); +int APS5_VABI ASN1_STRING_length_nid_postfix(const Asn1String* value); int APS5_VABI ASN1_STRING_print_nid_postfix(void); -int APS5_VABI ASN1_STRING_to_UTF8_nid_postfix(void); -int APS5_VABI ASN1_STRING_type_nid_postfix(void); +int APS5_VABI ASN1_STRING_to_UTF8_nid_postfix(unsigned char** output, Asn1String* value); +int APS5_VABI ASN1_STRING_type_nid_postfix(Asn1String* value); int APS5_VABI ASN1_TIME_print_nid_postfix(void); int APS5_VABI BIO_ctrl_nid_postfix(void); int APS5_VABI BIO_f_ssl_nid_postfix(void); diff --git a/core/libs/prx/libSceLibreSsl/src/Asn1.cpp b/core/libs/prx/libSceLibreSsl/src/Asn1.cpp index d83138ff0f..304ad8ad93 100644 --- a/core/libs/prx/libSceLibreSsl/src/Asn1.cpp +++ b/core/libs/prx/libSceLibreSsl/src/Asn1.cpp @@ -1,3 +1,5 @@ +#include "prx/libSceLibreSsl/libSceLibreSsl.hpp" +#include "prx/libc/include/Api.hpp" #include "prx/libc/include/General.hpp" #include #include @@ -6,17 +8,8 @@ #include #include -extern "C" void* APS5_VABI malloc_nid_postfix(std::size_t size); - namespace { -struct Asn1String { - std::int32_t length; - std::int32_t type; - unsigned char* data; - std::int64_t flags; -}; - static_assert(sizeof(Asn1String) == 24); static_assert(offsetof(Asn1String, data) == 8); static_assert(offsetof(Asn1String, flags) == 16); @@ -77,9 +70,9 @@ void AppendCharacter(std::vector& output, std::uint32_t value) { extern "C" { -std::int64_t APS5_VABI ASN1_INTEGER_get_nid_postfix(const void* value) { +std::int64_t APS5_VABI ASN1_INTEGER_get_nid_postfix(const Asn1String* value) { if (!value) return 0; - const auto& integer = *static_cast(value); + const auto& integer = *value; if (integer.type != 2 && integer.type != 0x102) return -1; if (integer.length > 8) return -1; if (integer.length <= 0 || !integer.data) return 0; @@ -93,24 +86,24 @@ std::int64_t APS5_VABI ASN1_INTEGER_get_nid_postfix(const void* value) { return static_cast(magnitude); } -unsigned char* APS5_VABI ASN1_STRING_data_nid_postfix(void* value) { +unsigned char* APS5_VABI ASN1_STRING_data_nid_postfix(Asn1String* value) { if (!value) NotImplemented_nid_no_patch(__func__); - return static_cast(value)->data; + return value->data; } -int APS5_VABI ASN1_STRING_length_nid_postfix(const void* value) { +int APS5_VABI ASN1_STRING_length_nid_postfix(const Asn1String* value) { if (!value) NotImplemented_nid_no_patch(__func__); - return static_cast(value)->length; + return value->length; } -int APS5_VABI ASN1_STRING_type_nid_postfix(void* value) { +int APS5_VABI ASN1_STRING_type_nid_postfix(Asn1String* value) { if (!value) NotImplemented_nid_no_patch(__func__); - return static_cast(value)->type; + return value->type; } -int APS5_VABI ASN1_STRING_to_UTF8_nid_postfix(unsigned char** output, void* value) { +int APS5_VABI ASN1_STRING_to_UTF8_nid_postfix(unsigned char** output, Asn1String* value) { if (!value) return -1; - const auto& string = *static_cast(value); + const auto& string = *value; const int width = CharacterWidth(string.type); if (width < 0) return -1; if (!output || (!string.data && string.length != 0)) NotImplemented_nid_no_patch(__func__); diff --git a/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp b/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp index 6ded0680c6..354f36a0c9 100644 --- a/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp +++ b/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp @@ -1,4 +1,5 @@ -#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceLibreSsl/libSceLibreSsl.hpp" +#include "prx/libc/include/Api.hpp" #include "prx/libc/include/ApplicationHeap.hpp" #include #include @@ -13,27 +14,11 @@ #include #include -extern "C" { -std::int64_t APS5_VABI ASN1_INTEGER_get_nid_postfix(const void* value); -unsigned char* APS5_VABI ASN1_STRING_data_nid_postfix(void* value); -int APS5_VABI ASN1_STRING_length_nid_postfix(const void* value); -int APS5_VABI ASN1_STRING_type_nid_postfix(void* value); -int APS5_VABI ASN1_STRING_to_UTF8_nid_postfix(unsigned char** output, void* value); -void APS5_VABI free_nid_postfix(void* pointer); -} - namespace { -struct String { - std::int32_t length; - std::int32_t type; - unsigned char* data; - std::int64_t flags; -}; - -static_assert(sizeof(String) == 24); -static_assert(offsetof(String, data) == 8); -static_assert(offsetof(String, flags) == 16); +static_assert(sizeof(Asn1String) == 24); +static_assert(offsetof(Asn1String, data) == 8); +static_assert(offsetof(Asn1String, flags) == 16); void Require(bool condition, const char* message) { if (condition) return; @@ -54,8 +39,8 @@ void RequireThrows(TAction action, const char* message) { void CheckConversion(int type, std::span input, std::span expected, int length = -2) { std::vector storage(input.begin(), input.end()); storage.push_back(0); - String value{length == -2 ? static_cast(input.size()) : length, type, storage.data(), 0x123456789abcdef}; - const String original = value; + Asn1String value{length == -2 ? static_cast(input.size()) : length, type, storage.data(), 0x123456789abcdef}; + const Asn1String original = value; unsigned char sentinel = 0xab; unsigned char* result = &sentinel; Require(ASN1_STRING_to_UTF8_nid_postfix(&result, &value) == static_cast(expected.size()), "UTF-8 byte length"); @@ -70,7 +55,7 @@ void CheckConversion(int type, std::span input, std::span::max()}; + Asn1String value{3, 12, data, std::numeric_limits::max()}; Require(ASN1_STRING_data_nid_postfix(&value) == data, "data accessor preserves pointer identity"); ASN1_STRING_data_nid_postfix(&value)[2] = 'c'; Require(data[2] == 'c', "data accessor permits writes"); @@ -92,12 +77,12 @@ void Integers() { 0, 0x01, 0x0123, 0x012345, 0x01234567, 0x0123456789, 0x0123456789ab, 0x0123456789abcd, 0x0123456789abcdef}; for (int length = 0; length <= 8; ++length) { - String value{length, 2, data, -1}; + Asn1String value{length, 2, data, -1}; Require(ASN1_INTEGER_get_nid_postfix(&value) == expected[length], "positive integer and 64-bit guest long"); value.type = 0x102; Require(ASN1_INTEGER_get_nid_postfix(&value) == -expected[length], "negative integer magnitude"); } - String value{9, 2, nullptr, 0}; + Asn1String value{9, 2, nullptr, 0}; Require(ASN1_INTEGER_get_nid_postfix(&value) == -1, "overlength is rejected before null data"); value.length = 8; Require(ASN1_INTEGER_get_nid_postfix(&value) == 0, "integer with null data"); @@ -105,7 +90,7 @@ void Integers() { for (int type : {2, 0x102}) { for (auto* bytes : {data, static_cast(nullptr), reinterpret_cast(1)}) { value = {length, type, bytes, -1}; - const String original = value; + const Asn1String original = value; Require(ASN1_INTEGER_get_nid_postfix(&value) == 0, "LibreSSL 2.5.4 nonpositive integer length"); Require(value.length == original.length && value.type == original.type && value.data == original.data && value.flags == original.flags, "nonpositive integer length preserves input"); @@ -124,7 +109,7 @@ void Integers() { Require(ASN1_INTEGER_get_nid_postfix(&value) == -std::numeric_limits::max(), "negative maximum magnitude"); std::array minimum{0x80, 0, 0, 0, 0, 0, 0, 0}; value = {8, 0x102, minimum.data(), -1}; - const String original = value; + const Asn1String original = value; Require(ASN1_INTEGER_get_nid_postfix(&value) == std::numeric_limits::min(), "minimum signed integer"); Require(value.length == original.length && value.type == original.type && value.data == original.data && value.flags == original.flags && minimum == std::array{0x80, 0, 0, 0, 0, 0, 0, 0}, "minimum integer preserves input"); @@ -159,7 +144,7 @@ void Strings() { CheckConversion(12, universalUtf8, universalUtf8); for (int type : {12, 19, 20, 22, 23, 24, 26, 28, 30}) { CheckConversion(type, {}, {}); - String empty{0, type, nullptr, 0}; + Asn1String empty{0, type, nullptr, 0}; unsigned char* result = nullptr; Require(ASN1_STRING_to_UTF8_nid_postfix(&result, &empty) == 0 && result && *result == 0, "empty null-data string"); free_nid_postfix(result); @@ -176,11 +161,11 @@ void Strings() { Require(ASN1_STRING_to_UTF8_nid_postfix(nullptr, nullptr) == -1, "null input checked before output"); for (int type = -1; type <= 32; ++type) { if (type == 12 || type == 19 || type == 20 || type == 22 || type == 23 || type == 24 || type == 26 || type == 28 || type == 30) continue; - String value{0, type, nullptr, 0}; + Asn1String value{0, type, nullptr, 0}; Require(ASN1_STRING_to_UTF8_nid_postfix(&result, &value) == -1 && result == &sentinel, "unsupported string tag"); Require(ASN1_STRING_to_UTF8_nid_postfix(nullptr, &value) == -1, "tag checked before output"); } - String value{0, 12, nullptr, 0}; + Asn1String value{0, 12, nullptr, 0}; RequireThrows([&] { ASN1_STRING_to_UTF8_nid_postfix(nullptr, &value); }, "null conversion output"); value.length = 1; RequireThrows([&] { ASN1_STRING_to_UTF8_nid_postfix(&result, &value); }, "null nonempty string data"); From 6508231c0ebfcd2a7774592c8239ee7d0ed619b6 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 21 Oct 2026 22:12:30 +0000 Subject: [PATCH 321/428] fix(libkernel): measure the TSC frequency on one CPU sceKernelGetTscFrequency times 20 ms of TSC ticks against the monotonic clock. A thread that moves to another CPU during the sleep reads its end value from another TSC. Where the TSCs of the CPUs differ, as on a Windows VM whose vCPU 0 counts about 3.8 billion ticks ahead of the others, the frequency came out as 0 or as 180 GHz. Pin the calibrating thread to the CPU it runs on and restore its affinity afterwards. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Time/Time.cpp | 42 +++++++++++++++++++++++++++ core/libs/tests/GuestCurrentCpu.cpp | 1 + 2 files changed, 43 insertions(+) diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 4e3d97ebca..3347e85747 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -37,6 +37,8 @@ #ifdef __linux__ #include #include +#include +#include #endif // __linux__ #endif @@ -163,6 +165,45 @@ static const bool g_timerSelfTest = [] { }(); #endif +namespace { + +class CurrentCpuPin { +public: + CurrentCpuPin() { +#ifdef _WIN32 + previous = SetThreadAffinityMask(GetCurrentThread(), DWORD_PTR{1} << GetCurrentProcessorNumber()); +#elif defined(__linux__) + const int cpu = sched_getcpu(); + if (cpu < 0 || cpu >= CPU_SETSIZE || pthread_getaffinity_np(pthread_self(), sizeof(previous), &previous) != 0) return; + cpu_set_t one; + CPU_ZERO(&one); + CPU_SET(cpu, &one); + pinned = pthread_setaffinity_np(pthread_self(), sizeof(one), &one) == 0; +#endif + } + + ~CurrentCpuPin() { +#ifdef _WIN32 + if (previous != 0) SetThreadAffinityMask(GetCurrentThread(), previous); +#elif defined(__linux__) + if (pinned) pthread_setaffinity_np(pthread_self(), sizeof(previous), &previous); +#endif + } + + CurrentCpuPin(const CurrentCpuPin&) = delete; + CurrentCpuPin& operator=(const CurrentCpuPin&) = delete; + +private: +#ifdef _WIN32 + DWORD_PTR previous = 0; +#elif defined(__linux__) + cpu_set_t previous{}; + bool pinned = false; +#endif +}; + +} + extern "C" { std::uint64_t APS5_VABI sceKernelGetProcessTime() { @@ -722,6 +763,7 @@ uint64_t APS5_VABI sceKernelReadTsc(void) { uint64_t APS5_VABI sceKernelGetTscFrequency(void) { static const std::uint64_t frequency = [] { + const CurrentCpuPin pin; const std::uint64_t startNanos = RawMonotonicNanos(); const std::uint64_t startTicks = __rdtsc(); TimedWait::SleepNanos(20000000ULL); diff --git a/core/libs/tests/GuestCurrentCpu.cpp b/core/libs/tests/GuestCurrentCpu.cpp index 17cda444cb..323eead874 100644 --- a/core/libs/tests/GuestCurrentCpu.cpp +++ b/core/libs/tests/GuestCurrentCpu.cpp @@ -33,6 +33,7 @@ int main() { const KernelCpumask available = sceKernelGetAvailableCpumask(); Require(sceKernelGetMainSocId() == 0 && sceKernelGetCpumode() == 0); Require(sceKernelGetCpuFrequency() > 0 && static_cast(sceKernelGetCpuFrequency()) == sceKernelGetTscFrequency()); + Require(sceKernelGetTscFrequency() >= 100000000u && sceKernelGetTscFrequency() <= 10000000000u); const std::array affinities{available, 0x1FFB, 0x3, 0x3F, 0x1000, 0x1, 0xFFFF, 0x10000}; for (const KernelCpumask affinity : affinities) { From de5306dd29180871c2010d434fdfcfa71c1f9056 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 21 Oct 2026 23:06:28 +0000 Subject: [PATCH 322/428] docs(debt): record the unpinned fallback of the TSC frequency measurement Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libkernel.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index fd41607f44..e77b404e69 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -130,6 +130,7 @@ - [scePthreadYield](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) on Windows calls Sleep(1) after 512 consecutive yields that find no other thread ready on the current processor and arrive less than ~10,000 TSC ticks apart (so the span varies with host CPU TSC rate); FreeBSD sched_yield never sleeps. - [__tls_get_addr](../../../core/libs/prx/libkernel/Module/src/Tls.cpp) (libkernel, NID `vNe1w4diLCs`) throws on macOS, where the relinker binds guest modules to `Aps5GuestTlsGetAddr_nid_no_patch`, which no library defines yet; it also throws for a null index, module 0 and a Windows thread without a block for the module. It reads the module word as the relinker writes it for `R_X86_64_DTPMOD64`: on Windows the address of the module's PE `_tls_index`, resolved through the thread's `ThreadLocalStoragePointer` as in [WindowsGuestStartup.cpp](../../../core/relinker/elfpatcher/src/windows/WindowsGuestStartup.cpp); on Linux the glibc module ID, passed to glibc's `__tls_get_addr`. Guest modules' imports are bound to the host resolver, so this export is reached only through a lookup by NID. +- [sceKernelGetTscFrequency](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) pins its thread to the CPU it runs on for the 20 ms measurement, so that both TSC reads come from one counter. If the pin can't be set (`sched_getcpu` or `pthread_getaffinity_np` fails on Linux, `SetThreadAffinityMask` fails on Windows), the measurement runs unpinned. On a host whose CPU TSCs differ, the result can then be wrong as before. Neither call is known to fail for the calling thread's own CPU, which is always in its allowed set. macOS never pins. - [Guest thread scheduling](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - the policies and priorities that `sched_get_priority_max`, `sched_get_priority_min`, `pthread_setschedparam` and `scePthreadSetschedparam` accept, and those a created thread takes, follow a PS5 (firmware 13.60), but they are only recorded and reported: they are not applied to host threads, so a title's priority order has no effect on host scheduling - [Linux runtime module initialization](../../../core/libs/prx/libkernel/Module/src/DynamicLoader.cpp) (libkernel) - modules in explicit runtime directories can be mapped for bindings before an explicit guest load runs their entry and init array once with the caller arguments. Finalizers run only after initialization completed. Console dependency-start and finalization ordering are unmeasured. Windows and macOS use their existing lifecycle paths. From 110f43a331d865b126e09e29e713f43c42176437 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Thu, 22 Oct 2026 04:16:46 +0200 Subject: [PATCH 323/428] feat(libSceFreeType): decompose guest outlines --- core/libs/prx/libSceFreeType/CMakeLists.txt | 1 + core/libs/prx/libSceFreeType/include/Api.hpp | 1 + .../libSceFreeType/include/GuestFreeType.hpp | 10 + .../libSceFreeType/src/OutlineDecompose.cpp | 130 ++++++++++ core/libs/tests/GuestFreeType.cpp | 238 ++++++++++++++++++ docs/dev/debt/libSceFreeType.md | 2 + 6 files changed, 382 insertions(+) create mode 100644 core/libs/prx/libSceFreeType/src/OutlineDecompose.cpp diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt index 4fec867345..fb0affb25c 100644 --- a/core/libs/prx/libSceFreeType/CMakeLists.txt +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -7,6 +7,7 @@ add_library(libSceFreeType SHARED EXCLUDE_FROM_ALL src/GuestObjects.cpp src/Names.cpp src/Trigonometry.cpp + src/OutlineDecompose.cpp ) target_include_directories(libSceFreeType PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceFreeType PRIVATE freetype) diff --git a/core/libs/prx/libSceFreeType/include/Api.hpp b/core/libs/prx/libSceFreeType/include/Api.hpp index dd8d748a21..819b837810 100644 --- a/core/libs/prx/libSceFreeType/include/Api.hpp +++ b/core/libs/prx/libSceFreeType/include/Api.hpp @@ -23,6 +23,7 @@ int APS5_VABI FT_Get_CID_From_Glyph_Index_nid_postfix(FreeType::GuestFaceRec* fa int APS5_VABI FT_Load_Char_nid_postfix(FreeType::GuestFaceRec* face, std::uint64_t charCode, std::int32_t loadFlags); int APS5_VABI FT_Outline_Get_Orientation_nid_postfix(FreeType::GuestOutline* outline); int APS5_VABI FT_Outline_Copy_nid_postfix(const FreeType::GuestOutline* source, FreeType::GuestOutline* target); +int APS5_VABI FT_Outline_Decompose_nid_postfix(FreeType::GuestOutline* outline, const FreeType::GuestOutlineFuncs* functions, void* user); int APS5_VABI FT_Render_Glyph_nid_postfix(FreeType::GuestGlyphSlotRec* slot, std::int32_t renderMode); int APS5_VABI FT_Select_Charmap_nid_postfix(FreeType::GuestFaceRec* face, std::int32_t encoding); int APS5_VABI FT_Set_Charmap_nid_postfix(FreeType::GuestFaceRec* face, FreeType::GuestCharMapRec* charmap); diff --git a/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp b/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp index 20553508a0..51acae2148 100644 --- a/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp +++ b/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp @@ -115,6 +115,15 @@ struct GuestOutline { std::int32_t flags; }; +struct GuestOutlineFuncs { + int (APS5_VABI *move_to)(const GuestVector*, void*); + int (APS5_VABI *line_to)(const GuestVector*, void*); + int (APS5_VABI *conic_to)(const GuestVector*, const GuestVector*, void*); + int (APS5_VABI *cubic_to)(const GuestVector*, const GuestVector*, const GuestVector*, void*); + std::int32_t shift; + std::int64_t delta; +}; + struct GuestGlyphSlotRec { void* library; void* face; @@ -185,6 +194,7 @@ static_assert(sizeof(GuestSizeRec) == 88 && offsetof(GuestSizeRec, metrics) == 2 static_assert(sizeof(GuestSizeRequestRec) == 32 && offsetof(GuestSizeRequestRec, width) == 8 && offsetof(GuestSizeRequestRec, height) == 16); static_assert(offsetof(GuestSizeRequestRec, horiResolution) == 24 && offsetof(GuestSizeRequestRec, vertResolution) == 28); static_assert(sizeof(GuestBitmap) == 40 && offsetof(GuestBitmap, buffer) == 16 && offsetof(GuestBitmap, palette) == 32); +static_assert(sizeof(GuestOutlineFuncs) == 48 && offsetof(GuestOutlineFuncs, shift) == 32 && offsetof(GuestOutlineFuncs, delta) == 40); static_assert(sizeof(GuestOutline) == 40 && offsetof(GuestOutline, points) == 8 && offsetof(GuestOutline, flags) == 32); static_assert(offsetof(GuestGlyphSlotRec, glyph_index) == 0x18 && offsetof(GuestGlyphSlotRec, metrics) == 0x30); static_assert(offsetof(GuestGlyphSlotRec, advance) == 0x80 && offsetof(GuestGlyphSlotRec, format) == 0x90); diff --git a/core/libs/prx/libSceFreeType/src/OutlineDecompose.cpp b/core/libs/prx/libSceFreeType/src/OutlineDecompose.cpp new file mode 100644 index 0000000000..2304dc5a9b --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/OutlineDecompose.cpp @@ -0,0 +1,130 @@ +#include "prx/libSceFreeType/include/GuestFreeType.hpp" + +#include +#include FT_FREETYPE_H + +#include +#include +#include + +using namespace FreeType; + +namespace { + +std::int64_t Scale(std::int64_t value, std::int32_t shift, std::int64_t delta) { + if (shift < 0 || shift >= 63) { + throw std::runtime_error("FT_Outline_Decompose: a shift outside 0-62 is not implemented"); + } + constexpr auto minimum = std::numeric_limits::min(); + constexpr auto maximum = std::numeric_limits::max(); + const std::int64_t factor = std::int64_t{1} << shift; + if (value < minimum / factor || value > maximum / factor) { + throw std::runtime_error("FT_Outline_Decompose: overflowing coordinate scaling is not implemented"); + } + const std::int64_t scaled = value * factor; + if ((delta > 0 && scaled < minimum + delta) || (delta < 0 && scaled > maximum + delta)) { + throw std::runtime_error("FT_Outline_Decompose: overflowing coordinate translation is not implemented"); + } + return scaled - delta; +} + +GuestVector Scale(GuestVector point, std::int32_t shift, std::int64_t delta) { + return {Scale(point.x, shift, delta), Scale(point.y, shift, delta)}; +} + +std::int64_t Midpoint(std::int64_t first, std::int64_t second) { + if ((second > 0 && first > std::numeric_limits::max() - second) || + (second < 0 && first < std::numeric_limits::min() - second)) { + throw std::runtime_error("FT_Outline_Decompose: overflowing midpoint arithmetic is not implemented"); + } + return (first + second) / 2; +} + +GuestVector Midpoint(GuestVector first, GuestVector second) { + return {Midpoint(first.x, second.x), Midpoint(first.y, second.y)}; +} + +} + +extern "C" int APS5_VABI FT_Outline_Decompose_nid_postfix(GuestOutline* outline, const GuestOutlineFuncs* functions, void* user) { + if (!outline) return FT_Err_Invalid_Outline; + if (!functions) return FT_Err_Invalid_Argument; + const std::int32_t shift = functions->shift; + const std::int64_t delta = functions->delta; + std::int32_t previousEnd = -1; + for (std::uint32_t contour = 0; contour < outline->n_contours; ++contour) { + const std::int32_t first = previousEnd + 1; + const std::int32_t last = outline->contours[contour]; + if (last < first) return FT_Err_Invalid_Outline; + previousEnd = last; + auto* points = outline->points; + auto* tags = outline->tags; + std::int32_t limit = last; + std::int32_t point = first; + GuestVector start = Scale(points[first], shift, delta); + const GuestVector end = Scale(points[last], shift, delta); + const int firstTag = tags[first] & 3; + if (firstTag == FT_CURVE_TAG_CUBIC) return FT_Err_Invalid_Outline; + if (firstTag == FT_CURVE_TAG_CONIC) { + if ((tags[last] & 3) == FT_CURVE_TAG_ON) { + start = end; + --limit; + } else { + start = Midpoint(start, end); + } + --point; + } + int error = functions->move_to(&start, user); + if (error != 0) return error; + bool closed = false; + while (point < limit) { + ++point; + const int tag = tags[point] & 3; + if (tag == FT_CURVE_TAG_ON) { + const GuestVector to = Scale(points[point], shift, delta); + error = functions->line_to(&to, user); + if (error != 0) return error; + } else if (tag == FT_CURVE_TAG_CONIC) { + GuestVector control = Scale(points[point], shift, delta); + while (true) { + if (point == limit) { + error = functions->conic_to(&control, &start, user); + closed = true; + break; + } + ++point; + const int nextTag = tags[point] & 3; + const GuestVector next = Scale(points[point], shift, delta); + if (nextTag == FT_CURVE_TAG_ON) { + error = functions->conic_to(&control, &next, user); + break; + } + if (nextTag != FT_CURVE_TAG_CONIC) return FT_Err_Invalid_Outline; + const GuestVector middle = Midpoint(control, next); + error = functions->conic_to(&control, &middle, user); + if (error != 0) return error; + control = next; + } + if (error != 0) return error; + } else { + if (point + 1 > limit || (tags[point + 1] & 3) != FT_CURVE_TAG_CUBIC) return FT_Err_Invalid_Outline; + const GuestVector control1 = Scale(points[point], shift, delta); + const GuestVector control2 = Scale(points[point + 1], shift, delta); + point += 2; + if (point <= limit) { + const GuestVector to = Scale(points[point], shift, delta); + error = functions->cubic_to(&control1, &control2, &to, user); + } else { + error = functions->cubic_to(&control1, &control2, &start, user); + closed = true; + } + if (error != 0) return error; + } + } + if (!closed) { + error = functions->line_to(&start, user); + if (error != 0) return error; + } + } + return 0; +} diff --git a/core/libs/tests/GuestFreeType.cpp b/core/libs/tests/GuestFreeType.cpp index eaf7f40652..856e3b8200 100644 --- a/core/libs/tests/GuestFreeType.cpp +++ b/core/libs/tests/GuestFreeType.cpp @@ -466,6 +466,243 @@ static void Trigonometry() { scalarCases, vectorCases, differenceCases, rendered); } +struct DecomposeEvents { + std::vector> calls; + std::size_t stopAt = static_cast(-1); + int result = 0; + + int Add(int kind, GuestVector first, GuestVector second = {}, GuestVector third = {}) { + calls.push_back({kind, first.x, first.y, second.x, second.y, third.x, third.y}); + return calls.size() - 1 == stopAt ? result : 0; + } +}; + +static GuestVector GuestPoint(const FT_Vector& point) { return {point.x, point.y}; } + +static int NativeMove(const FT_Vector* to, void* user) { return static_cast(user)->Add(1, GuestPoint(*to)); } +static int NativeLine(const FT_Vector* to, void* user) { return static_cast(user)->Add(2, GuestPoint(*to)); } +static int NativeConic(const FT_Vector* control, const FT_Vector* to, void* user) { + return static_cast(user)->Add(3, GuestPoint(*control), GuestPoint(*to)); +} +static int NativeCubic(const FT_Vector* first, const FT_Vector* second, const FT_Vector* to, void* user) { + return static_cast(user)->Add(4, GuestPoint(*first), GuestPoint(*second), GuestPoint(*to)); +} + +static int APS5_VABI GuestMove(const GuestVector* to, void* user) { return static_cast(user)->Add(1, *to); } +static int APS5_VABI GuestLine(const GuestVector* to, void* user) { return static_cast(user)->Add(2, *to); } +static int APS5_VABI GuestConic(const GuestVector* control, const GuestVector* to, void* user) { + return static_cast(user)->Add(3, *control, *to); +} +static int APS5_VABI GuestCubic(const GuestVector* first, const GuestVector* second, const GuestVector* to, void* user) { + return static_cast(user)->Add(4, *first, *second, *to); +} + +static int APS5_VABI NullUserMove(const GuestVector*, void* user) { + Require(user == nullptr); + return -17; +} + +static std::size_t CompareDecomposition(std::vector points, std::vector tags, + std::vector contours, int shift, std::int64_t delta, + std::size_t stopAt = static_cast(-1), int stopResult = 0) { + std::vector nativePoints; + for (const auto point : points) nativePoints.push_back({static_cast(point.x), static_cast(point.y)}); + GuestOutline outline{static_cast(contours.size()), static_cast(points.size()), points.data(), tags.data(), contours.data(), 0}; + FT_Outline native{outline.n_contours, outline.n_points, nativePoints.data(), tags.data(), contours.data(), 0}; + DecomposeEvents actual; + DecomposeEvents expected; + actual.stopAt = expected.stopAt = stopAt; + actual.result = expected.result = stopResult; + const GuestOutlineFuncs functions{GuestMove, GuestLine, GuestConic, GuestCubic, shift, delta}; + const FT_Outline_Funcs nativeFunctions{NativeMove, NativeLine, NativeConic, NativeCubic, shift, static_cast(delta)}; + const int result = FT_Outline_Decompose_nid_postfix(&outline, &functions, &actual); + Require(result == FT_Outline_Decompose(&native, &nativeFunctions, &expected)); + Require(actual.calls == expected.calls); + for (std::size_t index = 0; index < points.size(); ++index) { + Require(points[index].x == nativePoints[index].x && points[index].y == nativePoints[index].y); + } + if (stopAt < expected.calls.size()) Require(result == stopResult && actual.calls.size() == stopAt + 1); + return actual.calls.size(); +} + +struct ChangingDecomposition { + DecomposeEvents events; + GuestOutlineFuncs guest{}; + FT_Outline_Funcs native{}; + GuestOutline* outline = nullptr; + FT_Outline* nativeOutline = nullptr; + GuestVector* replacement = nullptr; + FT_Vector* nativeReplacement = nullptr; +}; + +static int NativeChangedLine(const FT_Vector* to, void* user) { + return static_cast(user)->events.Add(5, GuestPoint(*to)); +} +static int APS5_VABI GuestChangedLine(const GuestVector* to, void* user) { + return static_cast(user)->events.Add(5, *to); +} +static int NativeChangingMove(const FT_Vector* to, void* user) { + auto& state = *static_cast(user); + state.native.line_to = NativeChangedLine; + state.native.shift = 4; + state.native.delta = 999; + state.nativeOutline->points = state.nativeReplacement; + return state.events.Add(1, GuestPoint(*to)); +} +static int APS5_VABI GuestChangingMove(const GuestVector* to, void* user) { + auto& state = *static_cast(user); + state.guest.line_to = GuestChangedLine; + state.guest.shift = 4; + state.guest.delta = 999; + state.outline->points = state.replacement; + GuestOutline empty{}; + const GuestOutlineFuncs noCallbacks{}; + Require(FT_Outline_Decompose_nid_postfix(&empty, &noCallbacks, nullptr) == 0); + return state.events.Add(1, *to); +} + +static void OutlineDecomposition() { + GuestOutline empty{}; + const GuestOutlineFuncs callbacks{GuestMove, GuestLine, GuestConic, GuestCubic, 0, 0}; + const FT_Outline_Funcs nativeCallbacks{NativeMove, NativeLine, NativeConic, NativeCubic, 0, 0}; + Require(FT_Outline_Decompose_nid_postfix(nullptr, nullptr, nullptr) == FT_Outline_Decompose(nullptr, nullptr, nullptr)); + Require(FT_Outline_Decompose_nid_postfix(nullptr, &callbacks, nullptr) == FT_Outline_Decompose(nullptr, &nativeCallbacks, nullptr)); + Require(FT_Outline_Decompose_nid_postfix(&empty, nullptr, nullptr) == FT_Err_Invalid_Argument); + const GuestOutlineFuncs unused{nullptr, nullptr, nullptr, nullptr, -1, INT64_MIN}; + Require(FT_Outline_Decompose_nid_postfix(&empty, &unused, nullptr) == 0); + int scenarios = 0; + std::size_t calls = 0; + for (unsigned count = 1; count <= 6; ++count) { + std::vector points; + for (unsigned point = 0; point < count; ++point) points.push_back({static_cast(point) * 13 - 37, static_cast(point) * -17 + 23}); + const unsigned patterns = 1u << (2 * count); + for (unsigned pattern = 0; pattern < patterns; ++pattern) { + std::vector tags; + for (unsigned point = 0; point < count; ++point) tags.push_back(static_cast(((pattern >> (point * 2)) & 3) | 0xA4)); + for (const int shift : {0, 1, 4}) { + for (const int delta : {-17, 0, 19}) { + calls += CompareDecomposition(points, tags, {static_cast(count - 1)}, shift, delta); + ++scenarios; + } + } + } + } + const std::vector curve{{-17, 3}, {7, -5}, {29, 31}, {43, -11}, {59, 37}, {73, -13}, {89, 41}, {101, -17}}; + for (const auto& tags : std::vector>{{1, 0, 0, 1, 2, 2, 1, 0}, {1, 2, 2, 1, 0, 1, 2, 2}, {0, 0, 0, 0, 0, 0, 0, 0}}) { + for (const auto& contours : std::vector>{{7}, {2, 7}, {0, 3, 7}, {2, 1}}) { + const std::size_t events = CompareDecomposition(curve, tags, contours, 2, -3); + ++scenarios; + for (std::size_t stop = 0; stop < events; ++stop) { + for (const int error : {1, -17, 0x12345678, INT32_MIN}) { + calls += CompareDecomposition(curve, tags, contours, 2, -3, stop, error); + ++scenarios; + } + } + } + } + { + GuestVector points[]{{7, -5}, {11, 13}, {17, -19}, {23, 29}}; + FT_Vector nativePoints[]{{7, -5}, {11, 13}, {17, -19}, {23, 29}}; + GuestVector replacement[]{{107, 95}, {111, 113}, {117, 81}, {123, 129}}; + FT_Vector nativeReplacement[]{{107, 95}, {111, 113}, {117, 81}, {123, 129}}; + unsigned char tags[]{1, 1, 1, 1}; + unsigned short contours[]{1, 3}; + GuestOutline outline{2, 4, points, tags, contours, 0}; + FT_Outline native{2, 4, nativePoints, tags, contours, 0}; + ChangingDecomposition actual; + ChangingDecomposition expected; + actual.guest = {GuestChangingMove, nullptr, nullptr, nullptr, 1, -3}; + expected.native = {NativeChangingMove, nullptr, nullptr, nullptr, 1, -3}; + actual.outline = &outline; + actual.replacement = replacement; + expected.nativeOutline = &native; + expected.nativeReplacement = nativeReplacement; + Require(FT_Outline_Decompose_nid_postfix(&outline, &actual.guest, &actual) == FT_Outline_Decompose(&native, &expected.native, &expected)); + Require(actual.events.calls == expected.events.calls && actual.events.calls.size() == 6); + } + { + GuestVector point{7, -5}; + unsigned char tag = 1; + unsigned short contour = 0; + GuestOutline outline{1, 1, &point, &tag, &contour, 0}; + const GuestOutlineFuncs nullUser{NullUserMove, nullptr, nullptr, nullptr, 0, 0}; + Require(FT_Outline_Decompose_nid_postfix(&outline, &nullUser, nullptr) == -17); + } + { + GuestVector points[]{{0x10000000001ll, -0x10000000003ll}, {0x10000000005ll, -0x10000000007ll}, {0x10000000009ll, -0x1000000000Bll}}; + unsigned char tags[]{1, 1, 1}; + unsigned short contours[]{1}; + GuestOutline outline{1, 2, points, tags, contours, 0}; + auto functions = callbacks; + functions.shift = 2; + functions.delta = 0x100000000ll; + DecomposeEvents events; + Require(FT_Outline_Decompose_nid_postfix(&outline, &functions, &events) == 0); + const std::vector> expected{{1, 0x3FF00000004ll, -0x4010000000Cll, 0, 0, 0, 0}, + {2, 0x3FF00000014ll, -0x4010000001Cll, 0, 0, 0, 0}, + {2, 0x3FF00000004ll, -0x4010000000Cll, 0, 0, 0, 0}}; + Require(events.calls == expected); + tags[0] = tags[1] = 0; + events.calls.clear(); + Require(FT_Outline_Decompose_nid_postfix(&outline, &functions, &events) == 0); + const std::vector> conic{{1, 0x3FF0000000Cll, -0x40100000014ll, 0, 0, 0, 0}, + {3, 0x3FF00000004ll, -0x4010000000Cll, 0x3FF0000000Cll, -0x40100000014ll, 0, 0}, + {3, 0x3FF00000014ll, -0x4010000001Cll, 0x3FF0000000Cll, -0x40100000014ll, 0, 0}}; + Require(events.calls == conic); + tags[0] = 1; + tags[1] = tags[2] = 2; + contours[0] = 2; + outline.n_points = 3; + events.calls.clear(); + Require(FT_Outline_Decompose_nid_postfix(&outline, &functions, &events) == 0); + const std::vector> cubic{{1, 0x3FF00000004ll, -0x4010000000Cll, 0, 0, 0, 0}, + {4, 0x3FF00000014ll, -0x4010000001Cll, 0x3FF00000024ll, -0x4010000002Cll, 0x3FF00000004ll, -0x4010000000Cll}}; + Require(events.calls == cubic); + contours[0] = 1; + outline.n_points = 2; + for (const int shift : {-1, 63, INT32_MAX}) { + functions.shift = shift; + Require(ThrowsWith([&] { FT_Outline_Decompose_nid_postfix(&outline, &functions, &events); }, "FT_Outline_Decompose: a shift")); + } + functions.shift = 62; + functions.delta = 0; + Require(ThrowsWith([&] { FT_Outline_Decompose_nid_postfix(&outline, &functions, &events); }, "FT_Outline_Decompose: overflowing coordinate scaling")); + functions.shift = 0; + functions.delta = INT64_MIN; + Require(ThrowsWith([&] { FT_Outline_Decompose_nid_postfix(&outline, &functions, &events); }, "FT_Outline_Decompose: overflowing coordinate translation")); + functions.delta = 0; + points[0] = {INT64_MAX, 0}; + points[1] = {INT64_MAX, 0}; + tags[0] = tags[1] = 0; + Require(ThrowsWith([&] { FT_Outline_Decompose_nid_postfix(&outline, &functions, &events); }, "FT_Outline_Decompose: overflowing midpoint")); + } + const auto font = SquareGlyphFont('A', 700); + GuestLibrary* library = nullptr; + FT_Library oracle = nullptr; + GuestFaceRec* face = nullptr; + FT_Face native = nullptr; + Require(FT_Init_FreeType_nid_postfix(&library) == 0 && FT_Init_FreeType(&oracle) == 0); + Require(FT_New_Memory_Face_nid_postfix(library, font.data(), font.size(), 0, &face) == 0); + Require(FT_New_Memory_Face(oracle, font.data(), static_cast(font.size()), 0, &native) == 0); + int rendered = 0; + for (const unsigned pixels : {8u, 16u, 31u, 100u}) { + Require(FT_Set_Pixel_Sizes_nid_postfix(face, 0, pixels) == FT_Set_Pixel_Sizes(native, 0, pixels)); + for (const unsigned glyph : {0u, 1u}) { + Require(FT_Load_Glyph_nid_postfix(face, glyph, FT_LOAD_DEFAULT) == FT_Load_Glyph(native, glyph, FT_LOAD_DEFAULT)); + DecomposeEvents actual; + DecomposeEvents expected; + Require(FT_Outline_Decompose_nid_postfix(&face->glyph->outline, &callbacks, &actual) == FT_Outline_Decompose(&native->glyph->outline, &nativeCallbacks, &expected)); + Require(actual.calls == expected.calls); + SlotMatches(face->glyph, native->glyph); + Require(FT_Render_Glyph_nid_postfix(face->glyph, FT_RENDER_MODE_NORMAL) == FT_Render_Glyph(native->glyph, FT_RENDER_MODE_NORMAL)); + SlotMatches(face->glyph, native->glyph); + ++rendered; + } + } + Require(FT_Done_FreeType_nid_postfix(library) == 0 && FT_Done_FreeType(oracle) == 0); + std::printf("FreeType outline decomposition: %d native comparisons, %zu callbacks, %d rendered glyphs, wide coordinates and callback mutation\n", scenarios, calls, rendered); +} + struct Finalizers { std::vector calls; }; @@ -916,6 +1153,7 @@ int main() { std::filesystem::create_directories(root / "app0"); CheckGasp(); Trigonometry(); + OutlineDecomposition(); const std::vector font = SquareGlyphFont('A', 700); CheckOutlineCopies(); { diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index f62e84b5b7..61db3761e9 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -10,6 +10,8 @@ - [FT_Outline_Copy](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - compatibility with the console FreeType build and its errors is unverified. The LP64 guest adapter follows the unsigned counts and copy contract of bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c). +- [FT_Outline_Decompose](../../../core/libs/prx/libSceFreeType/src/OutlineDecompose.cpp) (libSceFreeType) - console callback ABI and FreeType-version equivalence are unverified; the LP64 adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c). Nonempty outlines with shifts outside 0-62 or signed 64-bit overflow in scaling, translation or midpoint sums throw; native behavior for that arithmetic is undefined. Callers must supply valid point/tag/contour arrays and the callbacks their contours use. + - [FT_Init_FreeType, FT_New_Face, FT_Set_Pixel_Sizes, FT_Get_Char_Index, FT_Load_Glyph, FT_Render_Glyph](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - the PS4 library is taken to be FreeType 2 built for LP64: the face, size, charmap, glyph slot, bitmap and outline layouts follow the public headers of the bundled FreeType 2.13.3 with a 64-bit `long`, which matches the offsets the OpenOrbis samples read (`face->glyph` at 0x98, `slot->advance` at 0x80, `slot->bitmap` at 0x98, `slot->bitmap_left` at 0xc0). Which FreeType version and options the console library is built with is unknown, so the behaviour and error codes are those of FreeType 2.13.3. The OpenOrbis font, keyboard, threading, system and SDL2 samples import these six functions - [FT_Bitmap_New, FT_Bitmap_Copy, FT_Bitmap_Convert, FT_Bitmap_Done](../../../core/libs/prx/libSceFreeType/src/Bitmap.cpp) (libSceFreeType) - console behaviour and error codes are unmeasured; these follow bundled FreeType 2.13.3. Copy and Convert throw when source and target share a non-null buffer; Convert also throws when they are the same object. Other overlapping storage remains forbidden by the native API contract From 472623ec33db324303f5fa891f5fca7c0e91294f Mon Sep 17 00:00:00 2001 From: orezraey Date: Sat, 24 Oct 2026 23:55:06 -0300 Subject: [PATCH 324/428] docs(libkernel): mark the AF_INET bind and connect check order unmeasured Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestSocketAddressLength.cpp | 1 - docs/dev/debt/libkernel.md | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/core/libs/tests/GuestSocketAddressLength.cpp b/core/libs/tests/GuestSocketAddressLength.cpp index 4f951e8df4..e2b7017eec 100644 --- a/core/libs/tests/GuestSocketAddressLength.cpp +++ b/core/libs/tests/GuestSocketAddressLength.cpp @@ -86,7 +86,6 @@ void LongAddressesAndOtherFamiliesFail() { Require(Fails(bind_nid_postfix(datagram, address.data(), 16), ErrorAddressFamilyNotSupported), "bind of another family on an AF_INET socket must fail with EAFNOSUPPORT (47)"); } address[1] = 28; - Require(Fails(bind_nid_postfix(datagram, address.data(), 28), ErrorAddressFamilyNotSupported), "the family is checked before the length on an AF_INET socket"); Require(Fails(sendto_nid_postfix(datagram, "x", 1, 0, address.data(), 28), ErrorInvalid), "sendto checks the length before the family"); Require(Fails(sendto_nid_postfix(datagram, "x", 1, 0, address.data(), 16), ErrorAddressFamilyNotSupported), "sendto of another family fails with EAFNOSUPPORT (47)"); address[1] = 2; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 11b399f990..9b3f1a60be 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -218,7 +218,7 @@ - [recv and recvfrom](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answer some cases without the host. A zero-length receive on a stream socket returns 0, or `ENOTCONN` when it is not connected ([L1566-L1578](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1566-L1578)), as on a PS5 (firmware 13.60). On an empty datagram socket it returns 0 at once; the console was measured only with `MSG_DONTWAIT`, and the blocking case follows FreeBSD 11.4's `soreceive_dgram`, which returns 0 before waiting. `recvmsg` answers the empty datagram socket the same way but asks the host in the stream cases. A datagram another thread takes after the check makes a zero-length receive wait - [recv and recvfrom after `shutdown(SHUT_RD)`](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) return 0 on Windows ([`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1556-L1562)) where Winsock fails with `EPIPE`; Linux and macOS still return data buffered before it, which the console discards. - [send, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - send and sendto without an address on an unconnected datagram socket map Windows `WSAENOTCONN` to FreeBSD's `EDESTADDRREQ` (39). -- [bind, connect, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) check a socket address as a payload process on a PS5 (firmware 13.60) did: a length below 2 fails with `EINVAL` and above 255 with `ENAMETOOLONG`; an `AF_INET` socket needs exactly 16 bytes and an `AF_INET6` socket 28, or `EINVAL`, and another family fails with `EAFNOSUPPORT`; `AF_UNSPEC` with `INADDR_ANY`, which FreeBSD's `bind` accepts, is unmeasured and fails too. On an `AF_INET` socket `bind` and `connect` check the family first and `sendto` the length first; an `AF_INET6` socket checks the length first for all three. Only datagram sockets were measured; stream sockets follow the same order. A connected stream socket's `sendto` ignores the address once its length passes, as `tcp_usr_send` does. `connect` on a listening stream socket fails with `EOPNOTSUPP` before these checks, as FreeBSD 11.4's `soconnect` does; that order was not measured +- [bind, connect, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) check a socket address as a payload process on a PS5 (firmware 13.60) did on datagram sockets: a length below 2 fails with `EINVAL` and above 255 with `ENAMETOOLONG`; an `AF_INET` socket needs exactly 16 bytes and an `AF_INET6` socket 28, or `EINVAL`; another family fails with `EAFNOSUPPORT`; `sendto` on an `AF_INET` socket, and all three on an `AF_INET6` socket, check the length first. Unmeasured: `bind` and `connect` on an `AF_INET` socket check the family first, where FreeBSD 11.4's `tcp_usr_bind` and `tcp_usr_connect` check the length first; stream sockets follow the datagram order; `AF_UNSPEC` with `INADDR_ANY`, which FreeBSD's `bind` accepts, fails; `connect` on a listening stream socket fails with `EOPNOTSUPP` first, as `soconnect` does. A connected stream socket's `sendto` ignores the address once its length passes, as `tcp_usr_send` does - [bind, connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) on an `AF_INET6` datagram socket with `IPV6_V6ONLY` cleared accept an IPv4-mapped address longer than 28 bytes and use its first 28, as FreeBSD 11.4's `udp6_bind` and `udp6_connect` hand it to `in_pcbbind` and `in_pcbconnect` as a `sockaddr_in` without the length test of `in6_pcbbind`; `sendto` still fails it with `EINVAL`, as `udp6_send` tests the length first. The console was measured only with `::1` and `IPV6_V6ONLY` at its default, so this case is not measured - [read, write and vector I/O](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. `EPIPE` from a write raises the host's `SIGPIPE` on Linux unless the socket has `SO_NOSIGPIPE` set, and the guest cannot ignore it: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; nothing is raised on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` From 38e535f939df1bcb74c1ee1a13fd6d37cf810ab7 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 22 Oct 2026 00:30:19 -0400 Subject: [PATCH 325/428] docs(debt): cite the bundled libc.prx for _Mtx_trylock, strdup and strndup Both libc debt entries said the console behaviour was unverified. The libc.prx that PPSA13934 ships in sce_module/ settles them (read from disassembly, not measured on a console): - _Mtx_trylock (0x5cf0) passes its argument to scePthreadMutexTrylock and returns 0 on success, 3 for SCE_KERNEL_ERROR_EBUSY and 4 for any other error. - strdup (0x6b440) calls strlen and strndup (0x6b650) calls strnlen on the source with no null check, so both fault on a null source, except strndup(NULL, 0), which returns an empty copy where this libc returns null with EINVAL. Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libc.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index bd27bf7a89..bd24fd0394 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -10,7 +10,7 @@ - [fdopen](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) validates descriptor access modes as FreeBSD 11.4 [fdopen.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/fdopen.c) does: a requested mode incompatible with a descriptor not opened for both reading and writing returns null with `EINVAL`, leaving the descriptor open. Windows reads the handle's granted access through the same host helper as `fcntl(F_GETFL)`; a closed descriptor returns null with `EBADF`. Console behavior is unverified. - [scriptingGetMem, scriptingFreeMem](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - runtime allocator hooks for Unity titles. The allocation signature `(alignment, size)`, minimum alignment 16 and null result for a non-power-of-two alignment follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libKernel.cpp); the free hook's one-pointer signature follows [Raeen's recorded contract](https://github.com/Whoisraeen/Raeen/blob/2db56923204b89719916b95e29c2f3c4e1d9f90b/docs/unity-dlsym-blocker.md). Both use libc's own guest heap, not the title's registered allocator. A title that ships its own `libc.prx` (Metal Gear Solid Master Collection, PPSA18194) has already initialized that allocator, and reaching it from here would run its initializer a second time. That title's IL2CPP runtime looks up only `scriptingGetMem`, by name from `Il2cppUserAssemblies.prx`, and asks it for 256 KiB blocks aligned to 4 KiB. Which console library exports the hooks is unknown, and there is no console measurement. Allocation failure and sizes that overflow return null. -- [_Mtx_trylock](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `k6pGNMwJB08`) - returns busy for a non-recursive mutex already owned by the caller, assumed to match `_Mtx_lock`'s ownership handling; exact console behavior is unverified. +- [_Mtx_trylock](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `k6pGNMwJB08`) - returns busy for a non-recursive mutex already owned by the caller. The `libc.prx` that titles ship (read from disassembly of PPSA13934's copy, 0x5cf0, not measured on a console) passes its argument to `scePthreadMutexTrylock` and returns 0 for success, `_Thrd_busy` (3) for `SCE_KERNEL_ERROR_EBUSY` and `_Thrd_error` (4) for any other error, so busy for an owned non-recursive mutex comes from the kernel mutex there; this libc keeps its own owner and count instead, and its result for a kernel error other than busy is not compared. - [nearbyintf](../../../core/libs/prx/libc/src/Math.cpp) (libc, NID `c+4r-T-tEIc`) rounds with the MXCSR rounding mode, as the console's `libSceLibcInternal.sprx` does in a payload process on a PS5 (firmware 13.60): with the x87 mode set to upward and MXCSR to nearest it rounded 2.5 to 2, and with MXCSR upward to 3, and the four modes gave the results the host gives. The console also raised `FE_INEXACT` for those inexact inputs, which the host's `nearbyintf` does not; whether it does for exact inputs was not measured - [__std_find_trivial_unsized_N](../../../core/libs/prx/libc/src/VectorAlgorithms.cpp) (libc) - matches the `libc.prx` bundled with PPSA08427 (disassembly, and that binary run side by side on random inputs): when nothing matches between `first` and the end of its 32-byte-aligned block, the console rescans that whole block without masking and so can return a match up to 31 bytes before `first` - [_Thrd_sleep](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `jfRI3snge3o`) follows the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`xtime.c`) and C11 Annex K, without console verification. It reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep` on POSIX, retrying on `EINTR`, or high-resolution waitable timers on Windows; a null xtime or one whose normalization overflows throws. Guest thread cancellation is not integrated with this wait. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched. @@ -50,7 +50,7 @@ - [strerror_s and strerrorlen_s](../../../core/libs/prx/libc/src/WideConversion.cpp) (libc) follow [C11 K.3.7.4.2 and K.3.7.4.3](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf), using the existing FreeBSD error messages and unknown-error formatting. Truncation writes an ellipsis when capacity exceeds 3; constraint failures leave the destination untouched and invoke the registered guest-ABI handler. The console's `RSIZE_MAX` (`SIZE_MAX >> 1` here), nonzero returns (`EINVAL` 22 for constraints, `ERANGE` 34 for truncation), handler arguments and default abort behavior are unverified. Messages remain in the C locale, and guest errno is preserved. - [getw and putw](../../../core/libs/prx/libc/src/BinaryWordIo.cpp) (libc) follow FreeBSD 11.4 [getw.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/getw.c) and [putw.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/putw.c): one 32-bit little-endian word, `getw` returns -1 for EOF or an error, and `putw` returns 0 on success or -1 on error while holding the stream lock. They use the existing host-backed streams, so buffering and error timing follow the host CRT. Null streams throw and closed streams return -1 with `EBADF`, as existing byte I/O does. Console behavior is unverified. A payload process on a PS5 (firmware 13.60) gave the same results: two words written little-endian, -1 with end of file after them and for a 2-byte file, and -1 with the error flag for `putw` on a read-only stream - [vsnwprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - `va_list` counterpart of `snwprintf_s` (C11 K.3.9.1.3): truncates to the buffer and returns the full length, with the same constraints and argument checks -- [strndup and strdup](../../../core/libs/prx/libc/src/AllocatingStrings.cpp) (libc) return null with `errno` set to `EINVAL` (22) when `source` is null, preventing a host segfault on invalid guest input. POSIX.1-2008 and FreeBSD 11.4 libc leave `strndup(NULL)` and `strdup(NULL)` undefined (faulting); exact console behavior on hardware is unverified. +- [strndup and strdup](../../../core/libs/prx/libc/src/AllocatingStrings.cpp) (libc) return null with `errno` set to `EINVAL` (22) when `source` is null, preventing a host segfault on invalid guest input. POSIX.1-2008 and FreeBSD 11.4 libc leave `strndup(NULL)` and `strdup(NULL)` undefined (faulting). The `libc.prx` that titles ship has no null check either (read from disassembly of PPSA13934's copy, not measured on a console): `strdup` (0x6b440) calls `strlen` and `strndup` (0x6b650) calls `strnlen` on the source before allocating, so both fault on a null source there, except `strndup(NULL, 0)`, whose `strnlen` reads nothing and which returns an empty copy; here it returns null with `EINVAL`. - [snprintf_s and vsnprintf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) - follow C11 K.3.5.3.5 and K.3.5.3.12 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`, as `snwprintf_s` does: a null buffer or format, `size` of 0 or above `RSIZE_MAX`, any `%n` and a null `%s`, `%ls` or `%S` argument return a negative value and set `buffer[0]` to zero when the buffer and size allow it; otherwise the output is truncated to the buffer and the full length is returned. A violation only returns: the constraint handler is not invoked, and the console's error value is unverified. Not confirmed on a PS5 title - [vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) - returns the count of items converted before an undersized `%s`/`%c`/`%[` destination (0 when it is the first), not EOF. Not confirmed on a PS5 title - [setlocale](../../../core/libs/prx/libc/src/StdioExtras.cpp) (libc) knows only the "C" locale, as the `libc.prx` that titles ship does: it ignores the category, returns "C" for a null, empty or "C" name and a null pointer for any other, "POSIX" included, without setting `errno`. Checked by running the copies bundled with PPSA07923 and PPSA13934 side by side for categories -40 to 79 and 2^30, not measured on a console. From 0e04fd6357cd13836e30d098d90e72ca6458bdcd Mon Sep 17 00:00:00 2001 From: camezaro-intc Date: Fri, 23 Oct 2026 13:10:29 -0600 Subject: [PATCH 326/428] fix(tests): support unittest discovery for NID patcher tests --- core/libs/nid/tests/test_elf_duplicates.py | 4 +-- core/libs/nid/tests/test_elf_layout.py | 4 +-- core/libs/nid/tests/test_gnu_hash.py | 5 ++-- core/libs/nid/tests/test_macho_nids.py | 9 +++++- core/libs/nid/tests/test_native_paths.py | 6 ++-- core/libs/nid/tests/test_pe_references.py | 5 ++-- core/libs/nid/tests/test_shared_strings.py | 4 +-- core/libs/nid/tests/test_symbol_versions.py | 31 ++++++++++++++++++++- core/libs/nid/tests/test_verneed.py | 4 +-- 9 files changed, 54 insertions(+), 18 deletions(-) diff --git a/core/libs/nid/tests/test_elf_duplicates.py b/core/libs/nid/tests/test_elf_duplicates.py index 504cf33bdc..25f9853da1 100644 --- a/core/libs/nid/tests/test_elf_duplicates.py +++ b/core/libs/nid/tests/test_elf_duplicates.py @@ -4,7 +4,7 @@ import sys import unittest from pathlib import Path -from test_symbol_versions import build_image, patch, read_dynsym, IMPORTS, NID_NAME, SHN_UNDEF, STT_FUNC +from test_symbol_versions import build_image, patch, read_dynsym, IMPORTS, NID_NAME, SHN_UNDEF, STT_FUNC, PATCHER def nid(name): @@ -21,6 +21,7 @@ def image(names, addresses=None): return bytes(build_image(entries, False)) +@unittest.skipUnless(PATCHER is not None and PATCHER.is_file(), "nid_patcher executable required") class ElfDuplicateTests(unittest.TestCase): def test_exports_patched_to_one_name_are_rejected(self): cases = ( @@ -65,5 +66,4 @@ class ElfDuplicateTests(unittest.TestCase): if __name__ == "__main__": - PATCHER = Path(sys.argv.pop(1)).resolve() unittest.main() diff --git a/core/libs/nid/tests/test_elf_layout.py b/core/libs/nid/tests/test_elf_layout.py index 703ac46db3..d4060852f6 100644 --- a/core/libs/nid/tests/test_elf_layout.py +++ b/core/libs/nid/tests/test_elf_layout.py @@ -3,9 +3,10 @@ import sys import unittest from pathlib import Path -from test_symbol_versions import build_image, symbols, patch, SHOFF +from test_symbol_versions import build_image, symbols, patch, SHOFF, PATCHER +@unittest.skipUnless(PATCHER is not None and PATCHER.is_file(), "nid_patcher executable required") class ElfLayoutTests(unittest.TestCase): def test_declared_layout_must_match_supported_tables(self): changes = ((5, "B", 2), (0x3a, "H", 32), @@ -44,5 +45,4 @@ class ElfLayoutTests(unittest.TestCase): if __name__ == "__main__": - PATCHER = Path(sys.argv.pop(1)).resolve() unittest.main() diff --git a/core/libs/nid/tests/test_gnu_hash.py b/core/libs/nid/tests/test_gnu_hash.py index 2e46a70f32..c1ddde43d4 100644 --- a/core/libs/nid/tests/test_gnu_hash.py +++ b/core/libs/nid/tests/test_gnu_hash.py @@ -3,7 +3,7 @@ import sys import unittest from pathlib import Path -from test_symbol_versions import build_image, symbols, patch, HASH_OFFSET, SHOFF, read_dynsym, gnu_hash, sections +from test_symbol_versions import build_image, symbols, patch, HASH_OFFSET, SHOFF, read_dynsym, gnu_hash, sections, PATCHER def sysv_hash(name): @@ -31,6 +31,7 @@ def sysv_lookup(image, name): return None +@unittest.skipUnless(PATCHER is not None and PATCHER.is_file(), "nid_patcher executable required") class GnuHashTests(unittest.TestCase): def image(self, buckets=4, bloom=1, shift=6, symoffset=3): image = build_image(symbols(False), False) @@ -95,6 +96,7 @@ class GnuHashTests(unittest.TestCase): self.assertEqual(struct.unpack_from(" 1: + patcher = Path(sys.argv[1]).resolve() + else: + patcher = PATCHER or find_patcher() + if patcher is None: + print("nid_patcher executable required") + sys.exit(0) with tempfile.TemporaryDirectory(prefix="anyps5-macho-nid-a\u00e7\u00e3o-\u65e5\u672c-") as directory: check_shrinking(patcher, directory) check_growing(patcher, directory) diff --git a/core/libs/nid/tests/test_native_paths.py b/core/libs/nid/tests/test_native_paths.py index ccfd9e6b8c..4b5c12193a 100644 --- a/core/libs/nid/tests/test_native_paths.py +++ b/core/libs/nid/tests/test_native_paths.py @@ -5,16 +5,14 @@ import tempfile import unittest from pathlib import Path -from test_macho_nids import build_dylib, exports_of, nid +from test_macho_nids import build_dylib, exports_of, nid, PATCHER try: import resource except ImportError: resource = None -PATCHER = Path(sys.argv.pop(1)).resolve() - - +@unittest.skipUnless(PATCHER is not None and PATCHER.is_file(), "nid_patcher executable required") class NativePathTests(unittest.TestCase): def invoke(self, *arguments): return subprocess.run([str(PATCHER), "libtest", *map(str, arguments)], diff --git a/core/libs/nid/tests/test_pe_references.py b/core/libs/nid/tests/test_pe_references.py index 2e6eed7f40..ae26b4c724 100644 --- a/core/libs/nid/tests/test_pe_references.py +++ b/core/libs/nid/tests/test_pe_references.py @@ -5,7 +5,7 @@ import tempfile import unittest from pathlib import Path -from test_macho_nids import build_dylib, exports_of, nid +from test_macho_nids import build_dylib, exports_of, nid, PATCHER def reference(*, headers=False, address=0x2000, ordinal=0): @@ -100,6 +100,7 @@ def forwarding_exports(image, bits): return string(table[4]), names, targets, addresses, (export_rva, export_size) +@unittest.skipUnless(PATCHER is not None and PATCHER.is_file(), "nid_patcher executable required") class ReferenceTests(unittest.TestCase): def patch_with_reference(self, data): with tempfile.TemporaryDirectory() as tmp: @@ -135,6 +136,7 @@ class ReferenceTests(unittest.TestCase): self.assertEqual(image, original) +@unittest.skipUnless(PATCHER is not None and PATCHER.is_file(), "nid_patcher executable required") class ForwardingExportTests(unittest.TestCase): def patch(self, image): with tempfile.TemporaryDirectory() as temporary: @@ -242,5 +244,4 @@ class ForwardingExportTests(unittest.TestCase): if __name__ == "__main__": - PATCHER = Path(sys.argv.pop(1)).resolve() unittest.main() diff --git a/core/libs/nid/tests/test_shared_strings.py b/core/libs/nid/tests/test_shared_strings.py index 3bfb64125b..0d4325d8cc 100644 --- a/core/libs/nid/tests/test_shared_strings.py +++ b/core/libs/nid/tests/test_shared_strings.py @@ -3,9 +3,10 @@ import sys import unittest from pathlib import Path -from test_symbol_versions import build_image, symbols, patch, SHOFF, DYNSYM_OFFSET, read_dynsym, sections, DEFINITIONS +from test_symbol_versions import build_image, symbols, patch, SHOFF, DYNSYM_OFFSET, read_dynsym, sections, DEFINITIONS, PATCHER +@unittest.skipUnless(PATCHER is not None and PATCHER.is_file(), "nid_patcher executable required") class SharedStringTests(unittest.TestCase): def test_dynamic_names_keep_original_string_when_export_shares_offset(self): for tag in (1, 14, 15, 29): @@ -54,5 +55,4 @@ class SharedStringTests(unittest.TestCase): if __name__ == "__main__": - PATCHER = Path(sys.argv.pop(1)).resolve() unittest.main() diff --git a/core/libs/nid/tests/test_symbol_versions.py b/core/libs/nid/tests/test_symbol_versions.py index b824a374c6..c9e9adc43f 100644 --- a/core/libs/nid/tests/test_symbol_versions.py +++ b/core/libs/nid/tests/test_symbol_versions.py @@ -1,3 +1,4 @@ +import os import re import struct import subprocess @@ -198,8 +199,36 @@ def test_unversioned_library_is_still_patched(patcher): assert len({entry[0] for entry in after if entry[0]}) == len([entry for entry in after if entry[0]]) +def find_patcher(): + for index, argument in enumerate(sys.argv[1:], 1): + candidate = Path(argument) + if candidate.stem == "nid_patcher" and candidate.is_file(): + sys.argv.pop(index) + return candidate.resolve() + env = os.environ.get("ANYPS5_NID_PATCHER") or os.environ.get("NID_PATCHER") + if env and Path(env).is_file(): + return Path(env).resolve() + repo_root = Path(__file__).resolve().parents[4] + for candidate in ( + repo_root / "build" / "core" / "libs" / "nid_patcher.exe", + repo_root / "build" / "core" / "libs" / "nid_patcher", + ): + if candidate.is_file(): + return candidate + return None + + +PATCHER = find_patcher() + + def main(): - patcher = Path(sys.argv[1]).resolve() + if len(sys.argv) > 1: + patcher = Path(sys.argv[1]).resolve() + else: + patcher = PATCHER or find_patcher() + if patcher is None: + print("nid_patcher executable required") + sys.exit(0) test_versioned_library_is_rejected(patcher) test_unversioned_library_is_still_patched(patcher) print("NID patcher symbol version tests passed") diff --git a/core/libs/nid/tests/test_verneed.py b/core/libs/nid/tests/test_verneed.py index 58ebc02c6b..79dd7a7789 100644 --- a/core/libs/nid/tests/test_verneed.py +++ b/core/libs/nid/tests/test_verneed.py @@ -3,7 +3,7 @@ import sys import unittest from pathlib import Path -from test_symbol_versions import build_image, symbols, patch, SHOFF, DYNSTR_OFFSET, sections +from test_symbol_versions import build_image, symbols, patch, SHOFF, DYNSTR_OFFSET, sections, PATCHER VERNEED = 0x580 @@ -22,6 +22,7 @@ def image(*, auxiliary=16, count=1, size=32, version=1, auxnext=0, nextrecord=0) return data +@unittest.skipUnless(PATCHER is not None and PATCHER.is_file(), "nid_patcher executable required") class VersionRequirementsTests(unittest.TestCase): def test_auxiliary_record_cannot_leave_declared_section(self): self.reject(image(auxiliary=32)) @@ -72,5 +73,4 @@ class VersionRequirementsTests(unittest.TestCase): if __name__ == "__main__": - PATCHER = Path(sys.argv.pop(1)).resolve() unittest.main() From 2f975066cdad751a0b8317a423b63986cae46414 Mon Sep 17 00:00:00 2001 From: Braxton Atkinson Date: Sat, 24 Oct 2026 23:26:59 -0500 Subject: [PATCH 327/428] fix(tools): parse flow-style pull-request filter keys safely --- tools/check_conventions.py | 31 ++++++++++++++ tools/tests/test_check_conventions_ci_gate.py | 41 +++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 9e95eb4bf6..8503faf71d 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -480,13 +480,44 @@ def block(lines, start): return lines[start + 1:end] +def flow_mapping(value): + tokens = re.findall(r'''"(?:\\.|[^"\\])*"|'(?:''|[^'])*'|[^{}\[\],:\s]+|[{}\[\],:]''', value) + if not tokens or tokens[0] != "{" or tokens[-1] != "}": + return {} + result = {} + stack = [] + start, colon = 1, None + for index, token in enumerate(tokens): + if token in ("{", "["): + stack.append(token) + elif token in ("}", "]"): + if not stack or stack.pop() != ("{" if token == "}" else "["): + return {} + if not stack and index != len(tokens) - 1: + return {} + if len(stack) == 1 and token == ":" and colon is None: + colon = index + if (len(stack) == 1 and token == ",") or (not stack and token == "}"): + if colon == start + 1: + result[tokens[start].strip("'\"")] = " ".join(tokens[colon + 1:index]) + elif start != index: + return {} + start, colon = index + 1, None + return result if not stack else {} + + def pull_request_filtered(lines, trigger): _, _, _, value = trigger if value: + if value.lstrip().startswith("{"): + triggers = flow_mapping(value) + return "pull_request" not in triggers or bool({"paths", "paths-ignore"} & flow_mapping(triggers["pull_request"]).keys()) return "pull_request" not in re.findall(r"[A-Za-z_]+", value) triggers = block(lines, lines.index(trigger)) for index, (_, indent, name, value) in enumerate(triggers): if name == "pull_request" and indent == triggers[0][1]: + if value: + return bool({"paths", "paths-ignore"} & flow_mapping(value).keys()) return any(entry[2] in ("paths", "paths-ignore") for entry in block(triggers, index)) return True diff --git a/tools/tests/test_check_conventions_ci_gate.py b/tools/tests/test_check_conventions_ci_gate.py index 200d32f39b..ca942cd702 100644 --- a/tools/tests/test_check_conventions_ci_gate.py +++ b/tools/tests/test_check_conventions_ci_gate.py @@ -86,6 +86,47 @@ class CiGateTests(unittest.TestCase): def test_paths_of_another_trigger_do_not_count(self): self.assertUngated(workflow(" push:\n paths:\n - 'core/**'\n pull_request:\n branches: [main]\n", " tests:\n runs-on: ubuntu-latest\n"), "tests") + def test_flow_mapping_paths_passes(self): + self.assertGated(workflow(" pull_request: {paths: ['core/**']}\n", " tests:\n runs-on: ubuntu-latest\n")) + + def test_flow_mapping_paths_ignore_passes(self): + self.assertGated(workflow(" pull_request: {paths-ignore: ['docs/**']}\n", " tests:\n runs-on: ubuntu-latest\n")) + + def test_flow_mapping_without_paths_fails(self): + self.assertUngated(workflow(" pull_request: {branches: [main]}\n", " tests:\n runs-on: ubuntu-latest\n"), "tests") + + def test_flow_mapping_quoted_values_do_not_count(self): + for filters in ( + "{branches-ignore: ['paths:unused']}", + '{branches-ignore: ["paths-ignore:unused"]}', + "{branches-ignore: ['it''s, paths:unused']}", + r'{branches-ignore: ["escaped\", paths:unused"]}', + "{branches-ignore: ['}, paths: [core/**], {']}", + ): + with self.subTest(filters=filters): + self.assertUngated(workflow(f" pull_request: {filters}\n", " tests:\n runs-on: ubuntu-latest\n"), "tests") + self.assertUngated(f"name: Test\non: {{pull_request: {filters}}}\njobs:\n tests:\n runs-on: ubuntu-latest\n", "tests") + + def test_flow_mapping_nested_paths_do_not_count(self): + self.assertUngated(workflow(" pull_request: {branches: [{paths: ['core/**']}]}\n", " tests:\n runs-on: ubuntu-latest\n"), "tests") + self.assertUngated("name: Test\non: {push: {paths: ['core/**']}, pull_request: {branches: [{paths-ignore: ['docs/**']}]}}\njobs:\n tests:\n runs-on: ubuntu-latest\n", "tests") + + def test_flow_mapping_quoted_keys_and_delimiters_pass(self): + for filters in ( + "{branches: ['release,}'], paths: ['core/**']}", + "{'paths': ['core/**']}", + '{"paths-ignore": ["docs/**"]}', + ): + with self.subTest(filters=filters): + self.assertGated(workflow(f" pull_request: {filters}\n", " tests:\n runs-on: ubuntu-latest\n")) + self.assertGated(f"name: Test\non: {{'pull_request': {filters}, push: {{branches: [main]}}}}\njobs:\n tests:\n runs-on: ubuntu-latest\n") + + def test_top_level_flow_mapping_paths_passes(self): + self.assertGated(f"name: Test\n\non: {{pull_request: {{paths: ['core/**']}}}}\njobs:\n{GATE} tests:\n runs-on: ubuntu-latest\n") + + def test_top_level_flow_paths_of_another_trigger_do_not_count(self): + self.assertUngated("name: Test\n\non: {push: {paths: ['core/**']}, pull_request: {branches: [main]}}\njobs:\n" + GATE + " tests:\n runs-on: ubuntu-latest\n", "tests") + def test_inline_trigger_list_fails(self): self.assertUngated("name: Test\non: [push, pull_request]\njobs:\n tests:\n runs-on: ubuntu-latest\n", "tests") From 4dcfc0b3db85a473ada9d19305c20067fc065f73 Mon Sep 17 00:00:00 2001 From: Braxton Atkinson Date: Sat, 24 Oct 2026 23:29:15 -0500 Subject: [PATCH 328/428] feat(libSceAppContent): implement sceAppContentGetEntitlementKey Resolve the rebase conflicts with upstream's public API header migration. Put the typed entitlement-key layout and signature in Api.hpp, remove duplicate test/source declarations, and retain upstream's typed add-content checks and initialization behavior. Keep ignoring the service label in the no-entitlement model. Cover labels 0, 7, 8 and UINT32_MAX without touching the output key. Update debt to distinguish firmware 13.42 module-side validation from the still-unverified system-service result. Evidence: https://github.com/boykopovar/AnyPS5/pull/9638#issuecomment-6390573735 Ubuntu 24.04, GCC 13.3, Release, upstream e7e3e9c1984c293f06bff08428b8c89b7b932ac2: actual libraries and guest_appcontent_tests built; guest_appcontent passes 1/1. Public-header audit, syntax checks of implementation and test, conventions, test registration and whitespace checks pass. No title or PS5 hardware tested; Windows was not rerun for this update. AI-assisted: yes --- core/libs/prx/libSceAppContent/Export.cpp | 9 ++++++--- core/libs/prx/libSceAppContent/include/Api.hpp | 6 +++++- core/libs/tests/GuestAppContent.cpp | 13 +++++++++++++ docs/dev/debt/libSceAppContent.md | 3 ++- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index e5c956f2ee..3485cd7b86 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -288,9 +288,12 @@ int APS5_VABI sceAppContentGetDownloadedStoreCountry(void) { return 0; } -int APS5_VABI sceAppContentGetEntitlementKey(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAppContentGetEntitlementKey(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel, AppContentEntitlementKey* key) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; + (void)serviceLabel; + if (!entitlementLabel || !key) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!validEntitlementLabel(*entitlementLabel, 0x01500000)) return SCE_APP_CONTENT_ERROR_PARAMETER; + return SCE_APP_CONTENT_ERROR_DRM_NO_ENTITLEMENT; } int APS5_VABI sceAppContentGetPftFlag(void) { diff --git a/core/libs/prx/libSceAppContent/include/Api.hpp b/core/libs/prx/libSceAppContent/include/Api.hpp index 938424fd88..cd0a799e37 100644 --- a/core/libs/prx/libSceAppContent/include/Api.hpp +++ b/core/libs/prx/libSceAppContent/include/Api.hpp @@ -7,6 +7,10 @@ struct AppContentAddcontInfo { uint32_t status; }; +struct AppContentEntitlementKey { + char data[16]; +}; + extern "C" { int APS5_VABI sceAppContentAddcontMount(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel, AppContentMountPoint* mountPoint); @@ -36,7 +40,7 @@ int APS5_VABI sceAppContentDownload1Shrink(void); int APS5_VABI sceAppContentGetAddcontInfoByEntitlementId(void); int APS5_VABI sceAppContentGetAddcontInfoListByIroTag(void); int APS5_VABI sceAppContentGetDownloadedStoreCountry(void); -int APS5_VABI sceAppContentGetEntitlementKey(void); +int APS5_VABI sceAppContentGetEntitlementKey(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel, AppContentEntitlementKey* key); int APS5_VABI sceAppContentGetPftFlag(void); int APS5_VABI sceAppContentGetRegion(void); int APS5_VABI sceAppContentRequestPatchInstall(void); diff --git a/core/libs/tests/GuestAppContent.cpp b/core/libs/tests/GuestAppContent.cpp index 876515f966..d39befaa2b 100644 --- a/core/libs/tests/GuestAppContent.cpp +++ b/core/libs/tests/GuestAppContent.cpp @@ -27,6 +27,7 @@ int main() { Require(sceAppContentAddcontUnmount(nullptr) == notInitialized); Require(sceAppContentGetAddcontInfo(0, nullptr, nullptr) == notInitialized); Require(sceAppContentGetAddcontInfoList(0, nullptr, 0, nullptr) == notInitialized); + Require(sceAppContentGetEntitlementKey(0, nullptr, nullptr) == notInitialized); Require(sceAppContentAppParamGetInt(0, nullptr) == notInitialized); Require(sceAppContentTemporaryDataMount2(0, nullptr) == notInitialized); Require(sceAppContentTemporaryDataUnmount(nullptr) == notInitialized); @@ -192,6 +193,16 @@ int main() { Require(std::memcmp(info, untouchedInfo, sizeof(info)) == 0); Require(sceAppContentGetAddcontInfo(0, nullptr, reinterpret_cast(info)) == ErrorParameter); Require(sceAppContentGetAddcontInfo(0, &label, nullptr) == ErrorParameter); + AppContentEntitlementKey key; + std::memset(&key, 0x5a, sizeof(key)); + const AppContentEntitlementKey untouchedKey = key; + for (const std::uint32_t serviceLabel : {0u, 7u, 8u, 0xffffffffu}) { + Require(sceAppContentGetEntitlementKey(serviceLabel, &label, &key) == ErrorDrmNoEntitlement); + Require(std::memcmp(&key, &untouchedKey, sizeof(key)) == 0); + } + Require(sceAppContentGetEntitlementKey(0, nullptr, &key) == ErrorParameter); + Require(std::memcmp(&key, &untouchedKey, sizeof(key)) == 0); + Require(sceAppContentGetEntitlementKey(0, &label, nullptr) == ErrorParameter); for (const std::uint32_t version : {0x014fffffu, 0x01500000u, 0x034fffffu, 0x03500000u}) { std::memcpy(process.data() + 16, &version, sizeof(version)); @@ -202,6 +213,8 @@ int main() { const int lateError = version >= 0x03500000 ? ErrorParameter : ErrorNotFound; Require(sceAppContentAddcontMount(0, &label, &mountPoint) == earlyError); Require(sceAppContentGetAddcontInfo(0, &label, reinterpret_cast(info)) == infoError); + Require(sceAppContentGetEntitlementKey(0, &label, &key) == infoError); + Require(std::memcmp(&key, &untouchedKey, sizeof(key)) == 0); Require(sceAppContentAddcontEnqueueDownload(0, &label) == earlyError); Require(sceAppContentAddcontDelete(0, &label) == lateError); Require(sceAppContentGetAddcontDownloadProgress(0, &label, progress) == lateError); diff --git a/docs/dev/debt/libSceAppContent.md b/docs/dev/debt/libSceAppContent.md index 1780d0bde0..1dbcd434cf 100644 --- a/docs/dev/debt/libSceAppContent.md +++ b/docs/dev/debt/libSceAppContent.md @@ -6,10 +6,11 @@ - [sceAppContentDownloadDataFormat](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - empties the `/download0` directory, creating it if needed. Imported by PPSA01317. A null mount point returns `PARAMETER` and another mount point returns `NOT_MOUNTED` as in `sceAppContentTemporaryDataFormat` and `sceAppContentDownloadDataGetAvailableSpaceKb`; the console's error codes for both and its behaviour with files still open are unverified -- [libSceAppContent pending exports](../../../core/libs/prx/libSceAppContent/Export.cpp) - 16 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAppContent.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceAppContent pending exports](../../../core/libs/prx/libSceAppContent/Export.cpp) - 15 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAppContent.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. ### Functional - [Initialization](../../../core/libs/prx/libSceAppContent/Export.cpp) - service readiness and boot attributes are not modeled; successful initialization returns a zero boot record. Initialization-state guards, SDK 0x01500000 repeated-initialization errors and reserved-field checks are recovered. SDK-dependent service-error translation remains incomplete. - [Additional content](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries; `sceAppContentAddcontEnqueueDownload` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no download is ever queued; `sceAppContentAddcontDelete` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no additional content is ever installed. `sceAppContentGetAddcontDownloadProgress` answers `PARAMETER` for a null label or progress and `NOT_FOUND` otherwise, as [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L2328-L2341) does for a label that is not installed additional content, and never writes the progress. Its 16-byte layout (`DataSize`, then `DownloadedSize`, both 64-bit) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L45-L54) and is unused here. The signatures of `sceAppContentAddcontEnqueueDownload` and `sceAppContentGetAddcontDownloadProgress` follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L140-L148), and the signature of `sceAppContentAddcontDelete` follows [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.h#L91-L92); dlc_emu is not a console measurement and no console source gives the error codes, so the signatures and the error codes are unverified +- [sceAppContentGetEntitlementKey](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - the service result without an entitlement is unverified: this model returns `DRM_NO_ENTITLEMENT` and leaves the 16-byte key unchanged. Initialization, null-pointer and SDK 0x01500000 label-padding checks match the reported [firmware 13.42 module probe](https://github.com/boykopovar/AnyPS5/pull/9638#issuecomment-6390573735), run in an emulator, not on hardware. That module forwards service labels 0, 7, 8 and 0xFFFFFFFF without checking them; this library ignores the service label. The system service's own validation and errors remain unknown. - [Temporary app-content storage](../../../core/libs/prx/libSceAppContent/Export.cpp) console behaviour and hardware return values are unverified, including behaviour with open files across unmount; host filesystem failures throw. Storage models one `/temp0` mount, preserving files on unmount while rejecting new path resolutions until remount, and leaving existing open host descriptors usable. `sceAppContentTemporaryDataMount` delegates to `sceAppContentTemporaryDataMount2` with option 1 (`TEMPORARY_DATA_OPTION_FORMAT`) to format on mount as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/app_content/app_content.cpp#L550-L553) does. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); pointer ABI follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs). From 455f7b093c732d8ccbe77f7ca3dff6bef3b16c41 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 02:23:05 +0000 Subject: [PATCH 329/428] fix(libc): add the year in asctime_s without signed overflow asctime_s and sceAmprAsctimeS added 1900 to tm_year in int before the range check, so a tm_year near INT_MAX overflowed: UndefinedBehaviorSanitizer reports signed integer overflow at SonyCrt.hpp:91. The year is now computed in 64 bits; the result stays EINVAL with an empty buffer for any year outside 0 to 9999. --- core/libs/prx/libc/include/SonyCrt.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/include/SonyCrt.hpp b/core/libs/prx/libc/include/SonyCrt.hpp index db53cd3d54..fc97c753f1 100644 --- a/core/libs/prx/libc/include/SonyCrt.hpp +++ b/core/libs/prx/libc/include/SonyCrt.hpp @@ -88,7 +88,7 @@ inline int SonyFormatAsctime(char* buffer, std::size_t size, const GuestTm& time static const char* const days[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; static const char* const months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; if (!buffer || size < 26) return EINVAL; - const int year = 1900 + time.tm_year; + const std::int64_t year = std::int64_t{time.tm_year} + 1900; if (time.tm_wday < 0 || time.tm_wday > 6 || time.tm_mon < 0 || time.tm_mon > 11 || time.tm_mday < 1 || time.tm_mday > 31 || time.tm_hour < 0 || time.tm_hour > 23 || time.tm_min < 0 || time.tm_min > 59 || time.tm_sec < 0 || time.tm_sec > 60 || @@ -97,7 +97,7 @@ inline int SonyFormatAsctime(char* buffer, std::size_t size, const GuestTm& time return EINVAL; } std::snprintf(buffer, size, "%.3s %.3s %2d %.2d:%.2d:%.2d %4d\n", days[time.tm_wday], months[time.tm_mon], - time.tm_mday, time.tm_hour, time.tm_min, time.tm_sec, year); + time.tm_mday, time.tm_hour, time.tm_min, time.tm_sec, static_cast(year)); return 0; } From 4c3aed7b3277baf17c209db580293dc70b375470 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 02:23:06 +0000 Subject: [PATCH 330/428] test(libc): cover asctime_s with extreme tm_year values A valid broken-down time with tm_year INT_MAX, INT_MAX - 1899 and INT_MIN must fail with EINVAL and an empty buffer through both asctime_s exports. Run under UndefinedBehaviorSanitizer, the case reports signed integer overflow without the fix. --- core/libs/tests/GuestCrt.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/libs/tests/GuestCrt.cpp b/core/libs/tests/GuestCrt.cpp index fc18d84627..1e8f1887f5 100644 --- a/core/libs/tests/GuestCrt.cpp +++ b/core/libs/tests/GuestCrt.cpp @@ -173,6 +173,13 @@ int main() { known.tm_year = 9000; text[0] = 'x'; Require(sceAmprAsctimeS(text, sizeof(text), &known) == EINVAL && text[0] == '\0'); + for (const int year : {INT_MAX, INT_MAX - 1899, INT_MIN}) { + known.tm_year = year; + text[0] = 'x'; + Require(sceAmprAsctimeS(text, sizeof(text), &known) == EINVAL && text[0] == '\0'); + text[0] = 'x'; + Require(asctime_s_nid_postfix(text, sizeof(text), &known) == EINVAL && text[0] == '\0'); + } known.tm_year = 70; known.tm_mday = 32; text[0] = 'x'; From 0f9fff204851709f572569a29665ab125f306133 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 23:30:04 +0000 Subject: [PATCH 331/428] fix(libc): fail ungetwc on a closed stream with EBADF ungetwc on a stream that a failed freopen closed reached GetHandle() and threw out of the export. ungetc and the other narrow stream calls return failure with errno 9. --- core/libs/prx/libc/src/StandardStreams.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libc/src/StandardStreams.cpp b/core/libs/prx/libc/src/StandardStreams.cpp index 958616a565..b40f822753 100644 --- a/core/libs/prx/libc/src/StandardStreams.cpp +++ b/core/libs/prx/libc/src/StandardStreams.cpp @@ -125,6 +125,7 @@ std::int32_t APS5_VABI fgetwc_nid_postfix(FileStream* stream) { } std::int32_t APS5_VABI ungetwc_nid_postfix(std::int32_t value, FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } if (value == -1) return -1; if (value < 0 || value > 255) { errno = 86; From de8cd096b21b40b14caa725cb2f252350efb3690 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 23:30:04 +0000 Subject: [PATCH 332/428] fix(libc): fail fputwc and fputws on a closed stream with EBADF The write threw inside GetNativeStream, which the wide writers catch and report as EINVAL. fputc, fputs and vfwprintf fail a closed stream with errno 9; putwc and putwchar go through fputwc. --- core/libs/prx/libc/src/FormattingWide.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/prx/libc/src/FormattingWide.cpp b/core/libs/prx/libc/src/FormattingWide.cpp index 66fd9d1142..6d1a58e4e5 100644 --- a/core/libs/prx/libc/src/FormattingWide.cpp +++ b/core/libs/prx/libc/src/FormattingWide.cpp @@ -362,6 +362,7 @@ int APS5_VABI vwprintf_nid_postfix(const char16_t* format, VaList* args) { int APS5_VABI fputwc_nid_postfix(char16_t value, FileStream* stream) { if (stream == nullptr) { errno = 22; return -1; } + if (!stream->IsOpen()) { errno = 9; return -1; } try { const std::string bytes = ToUtf8(std::u16string(1, value)); auto* native = GetNativeStream(stream); @@ -376,6 +377,7 @@ int APS5_VABI fputwc_nid_postfix(char16_t value, FileStream* stream) { int APS5_VABI fputws_nid_postfix(const char16_t* str, FileStream* stream) { if (str == nullptr || stream == nullptr) { errno = 22; return -1; } + if (!stream->IsOpen()) { errno = 9; return -1; } try { const std::u16string text(str); const std::string bytes = ToUtf8(text); From 1cf6734bf9bc3368d60da7f444539a1fa7ecf3ef Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 23:30:04 +0000 Subject: [PATCH 333/428] fix(libc): fail wprintf on a closed standard output with EBADF printf, puts and vwprintf already return -1 with errno 9 once a failed freopen closed stdout. wprintf reported EINVAL. --- core/libs/prx/libc/src/FormattingWide.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libc/src/FormattingWide.cpp b/core/libs/prx/libc/src/FormattingWide.cpp index 6d1a58e4e5..67a7fce10f 100644 --- a/core/libs/prx/libc/src/FormattingWide.cpp +++ b/core/libs/prx/libc/src/FormattingWide.cpp @@ -321,6 +321,7 @@ int APS5_VABI vsnwprintf_s_nid_postfix(char16_t* buffer, std::size_t size, const } int APS5_VABI wprintf_nid_postfix(const char16_t* format, ...) { + if (!__stdoutp_nid_postfix->IsOpen()) { errno = 9; return -1; } APS5_VA_BEGIN(format); int result = -1; try { From c2efce8ab7fd7349cb568bd4c717434ba28473ca Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 23:30:05 +0000 Subject: [PATCH 334/428] test(libc): cover the wide calls on a closed stream --- core/libs/tests/GuestStandardStreams.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index 0f459b1e83..f2d65cf2a4 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -539,6 +539,16 @@ static void CheckClosedStreamWrites() { Require(fseeko_nid_postfix(&stream, 0, SEEK_SET) == -1 && *__error_nid_postfix() == 9); Require(ftello_nid_postfix(&stream) == -1 && *__error_nid_postfix() == 9); Require(fileno_nid_postfix(&stream) == -1 && *__error_nid_postfix() == 9); + *__error_nid_postfix() = 0; + Require(fputwc_nid_postfix(u't', &stream) == -1 && *__error_nid_postfix() == 9); + *__error_nid_postfix() = 0; + Require(putwc_nid_postfix(u't', &stream) == -1 && *__error_nid_postfix() == 9); + *__error_nid_postfix() = 0; + Require(fputws_nid_postfix(u"text", &stream) == -1 && *__error_nid_postfix() == 9); + *__error_nid_postfix() = 0; + Require(ungetwc_nid_postfix('t', &stream) == -1 && *__error_nid_postfix() == 9); + *__error_nid_postfix() = 0; + Require(ungetwc_nid_postfix(0x100, &stream) == -1 && *__error_nid_postfix() == 9); Require(ferror_nid_postfix(&stream) == 0 && feof_nid_postfix(&stream) == 0); Require(fclose_nid_postfix(&stream) == -1 && *__error_nid_postfix() == 9); } @@ -549,6 +559,10 @@ static int CheckClosedStandardOutput() { Require(puts_nid_postfix("text") == -1 && *__error_nid_postfix() == 9); Require(PrintStandard("%d", 1) == -1 && *__error_nid_postfix() == 9); Require(fputs_nid_postfix("text", __stdoutp_nid_postfix) == -1 && *__error_nid_postfix() == 9); + *__error_nid_postfix() = 0; + Require(wprintf_nid_postfix(u"%d", 1) == -1 && *__error_nid_postfix() == 9); + *__error_nid_postfix() = 0; + Require(putwchar_nid_postfix(u't') == -1 && *__error_nid_postfix() == 9); Require(std::fputs("host output\n", stdout) >= 0 && std::fflush(stdout) == 0 && !std::ferror(stdout)); Require(freopen_nid_postfix("missing-closed-stream-dir/err.log", "a", __stderrp_nid_postfix) == nullptr); Require(std::fputs("host error\n", stderr) >= 0 && std::fflush(stderr) == 0 && !std::ferror(stderr)); From 443388f161b843a21af8446682695c0b9b2c754d Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 22:56:18 +0000 Subject: [PATCH 335/428] fix(tools): refuse unwrap_self segments placed before the file start --- tools/unwrap_self.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/unwrap_self.py b/tools/unwrap_self.py index 484334e200..ca98d9e02c 100644 --- a/tools/unwrap_self.py +++ b/tools/unwrap_self.py @@ -218,7 +218,7 @@ def place_segments(data, headers, records): for header in headers: if header["type"] == PT_LOAD and header["size"] == 0: continue - if header["source"] > len(data) or header["size"] > len(data) - header["source"]: + if header["source"] < 0 or header["source"] > len(data) or header["size"] > len(data) - header["source"]: raise Refused("segment %d does not fit the file" % header["index"]) From 4f078782a143c63a0b634eb7e3e80923cff4515d Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sat, 24 Oct 2026 22:56:52 +0000 Subject: [PATCH 336/428] test(tools): cover unwrap_self segments with stale offsets that cannot be placed --- tools/tests/test_unwrap_self.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index b8bcdd092f..59d334cfc1 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -117,6 +117,34 @@ class UnwrapTest(unittest.TestCase): self.assertEqual(code, 0) self.assertTrue(output.exists()) + def test_segment_stale_offset_before_the_recorded_shift_is_refused(self): + segments = default_segments() + [(SCE_COMMENT, 4, 0x100, 0, 0x20, 1)] + container = build_container(segments, default_records(), default_payloads()) + source = Path(self.directory) / "input.self" + output = Path(self.directory) / "output.elf" + source.write_bytes(container) + result = subprocess.run([sys.executable, str(Path(unwrap_self.__file__)), str(source), str(output)], + capture_output=True, text=True, timeout=30) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertIn("does not fit the file", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertFalse(output.exists()) + + def test_huge_stale_offset_is_refused_instead_of_exhausting_memory(self): + segments = default_segments() + segments[2] = (PT_DYNAMIC, 6, 0x49000000001580, 0x1040, 0x10, 8) + segments.append((SCE_COMMENT, 4, 0x1600, 0, 0x4000000000000000, 1)) + container = build_container(segments, default_records(), default_payloads()) + source = Path(self.directory) / "input.self" + output = Path(self.directory) / "output.elf" + source.write_bytes(container) + result = subprocess.run([sys.executable, str(Path(unwrap_self.__file__)), str(source), str(output)], + capture_output=True, text=True, timeout=30) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertIn("does not fit the file", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertFalse(output.exists()) + def test_write_failure_preserves_output_and_removes_temporary_files(self): directory = Path(self.directory) source = directory / "input.self" From a2df041469c1a738f21b118c17ad133a4e313a06 Mon Sep 17 00:00:00 2001 From: hypermist Date: Sun, 25 Oct 2026 19:49:25 +1300 Subject: [PATCH 337/428] docs(libSceFont): state the font metrics gap without naming a source Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libSceFont.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index c59aa9fb2b..056bf25328 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -22,7 +22,7 @@ - [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - the other script files of a set are chosen from its type, not measured on a console: Vietnamese for low-byte bit 0x10, Thai for script bit 0x10, Japanese for 0x04, simplified Chinese for 0x80 (before Japanese with 0x08), Hangul for 0x20 and Arabic for low-byte bit 0x80, as the SDK set names (`..._JP_AR`, `..._GBCJK_TH`) listed by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/font/font_internal.cpp) suggest; the file order and the Hangul file name (`SCEPS4Yoongd-Medium.otf`, `SCEPS4Yoongd-Bold.otf`) also come from shadPS4. A character the primary file lacks is taken from the first of them that has it; kerning between two files is 0, and `sceFontGetCharGlyphCode` and `sceFontGetFontGlyphsCount` answer for the primary file only - [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - the console file names are those of the PS4 firmware; the names of the PS5-only JG2 sets (`0x1A......`) are unknown, so they use the SSTJpPro files. When a console file is missing, an openly licensed substitute (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans Arabic, Noto Sans CJK with its Korean face for Hangul) is loaded instead; its metrics differ, so text width, line height and wrapping differ from the console. System font faces are loaded by FreeType in host memory, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. - [sceFontGetCharGlyphMetrics, sceFontGetRenderCharGlyphMetrics](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) report the vertical metrics the way the [FreeType driver](../../../core/libs/prx/libSceFontFt/src/Driver.cpp) reports them for a rendered glyph, not confirmed on a title: FreeType's vertical advance, and for a face without vertical metrics a bearing of half the horizontal advance left of the horizontal bearing and of half the advance left over by the glyph height. -- [sceFontGetFontMetrics](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) - the 64-byte output, the form fallbacks (2 and 3 fall back to the `hhea` values when OS/2 is missing or gives an empty range, 4 builds vertical values from `vhea` or OS/2) and the raw weight deltas and slant copied from the style are read from the disassembly of firmware 13.60 `libSceFont.sprx` and `libSceFontFt.sprx`, not measured on a console. For a font set the values are those of its primary file; which file the console reports is not recovered. Closes when: measured on a PS5 +- [sceFontGetFontMetrics](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) - the 64-byte output, the form fallbacks (2 and 3 fall back to the `hhea` values when OS/2 is missing or gives an empty range, 4 builds vertical values from `vhea` or OS/2) and the raw weight deltas and slant copied from the style are not measured on a console. For a font set the values are those of its primary file; which file the console reports is not recovered. Closes when: measured on a PS5 - [Font writing lines](../../../core/libs/prx/libSceFont/src/WritingLine.cpp): visual ordering and spacing originated from PS4 measurements and match the supplied PS5 structure and control flow. Exhaustive floating-point equivalence, including NaN payloads and rounding, has not been verified against a running PS5 module. - [Font graphics and metric signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): graphics-device/service creation takes font memory, parameters and a handle output; destruction takes a handle pointer. Attribute selectors and outputs are 32-bit values. Outline-profile, style-information and expansion-buffer outputs remain opaque pending field recovery. Protected implementations are unchanged. From c46dfd259d447d6fd9589c464817dea18c9d9610 Mon Sep 17 00:00:00 2001 From: hypermist Date: Sun, 25 Oct 2026 19:50:06 +1300 Subject: [PATCH 338/428] docs(libSceFont): state the font style information gap without naming a source Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libSceFont.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index 4bdb5ca98c..6f607015d9 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -23,7 +23,7 @@ - [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - the console file names are those of the PS4 firmware; the names of the PS5-only JG2 sets (`0x1A......`) are unknown, so they use the SSTJpPro files. When a console file is missing, an openly licensed substitute (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans Arabic, Noto Sans CJK with its Korean face for Hangul) is loaded instead; its metrics differ, so text width, line height and wrapping differ from the console. System font faces are loaded by FreeType in host memory, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. - [sceFontGetCharGlyphMetrics, sceFontGetRenderCharGlyphMetrics](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) report the vertical metrics the way the [FreeType driver](../../../core/libs/prx/libSceFontFt/src/Driver.cpp) reports them for a rendered glyph, not confirmed on a title: FreeType's vertical advance, and for a face without vertical metrics a bearing of half the horizontal advance left of the horizontal bearing and of half the advance left over by the glyph height. - [sceFontGetFontMetrics](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) - the 64-byte output, the form fallbacks (2 and 3 fall back to the `hhea` values when OS/2 is missing or gives an empty range, 4 builds vertical values from `vhea` or OS/2) and the raw weight deltas and slant copied from the style are not measured on a console. For a font set the values are those of its primary file; which file the console reports is not recovered. Closes when: measured on a PS5 -- [sceFontGetFontStyleInformation](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) - the 24-byte output (FreeType family and style names, italic and bold flags, OS/2 weight class when it is a multiple of 100 from 100 to 900, width class below 10, and 1 plus a right-to-left `head` direction plus 0x80 for vertical metrics) and the font-set record path are read from the disassembly of firmware 13.60 `libSceFont.sprx` and `libSceFontFt.sprx`, not measured on a console. A face without a `head` table, such as a BDF font, throws: the console reads the table without checking it. Closes when: measured on a PS5 +- [sceFontGetFontStyleInformation](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) - the 24-byte output (FreeType family and style names, italic and bold flags, OS/2 weight class when it is a multiple of 100 from 100 to 900, width class below 10, and 1 plus a right-to-left `head` direction plus 0x80 for vertical metrics) and the font-set record path are not measured on a console. A face without a `head` table, such as a BDF font, throws, as its console result is unknown. Closes when: measured on a PS5 - [Font writing lines](../../../core/libs/prx/libSceFont/src/WritingLine.cpp): visual ordering and spacing originated from PS4 measurements and match the supplied PS5 structure and control flow. Exhaustive floating-point equivalence, including NaN payloads and rounding, has not been verified against a running PS5 module. - [Font graphics and metric signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): graphics-device/service creation takes font memory, parameters and a handle output; destruction takes a handle pointer. Attribute selectors and outputs are 32-bit values. Outline-profile and expansion-buffer outputs remain opaque pending field recovery. Protected implementations are unchanged. From 545ba1a76b4aafdd93050c3d25cf526af9c5cad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 25 Oct 2026 07:54:12 +0100 Subject: [PATCH 339/428] fix(main): job manager spin test takes the quietest of 3 no-pending samples Culprit: none by code; run 40167585870 (main 2400596055) failed guest_job_manager_worker_spin (check at line 688: noPending 27 ms vs parked 10.6 ms + interval/3 8.5 ms). The sample only adds CPU when the runner is noisy, so the minimum of 3 is the right estimator. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestJobManager.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestJobManager.cpp b/core/libs/tests/GuestJobManager.cpp index 84796724b5..af4995720a 100644 --- a/core/libs/tests/GuestJobManager.cpp +++ b/core/libs/tests/GuestJobManager.cpp @@ -670,11 +670,22 @@ static SpinSample MeasureWorkerSpin(std::uint32_t spin, std::uint32_t rounds, bo return {consumed, clock.reference}; } +static SpinSample QuietestWorkerSpin(std::uint32_t spin, std::uint32_t rounds, bool pending) { + auto best = MeasureWorkerSpin(spin, rounds, pending); + for (int attempt = 1; attempt < 3; ++attempt) { + const auto sample = MeasureWorkerSpin(spin, rounds, pending); + if (sample.consumed < best.consumed) { + best = sample; + } + } + return best; +} + static void WorkerSpinCase() { constexpr std::uint32_t rounds = 512; const auto parked = MeasureWorkerSpin(0, rounds, true); const auto spinning = MeasureWorkerSpin(50, rounds, true); - const auto noPending = MeasureWorkerSpin(50, rounds, false); + const auto noPending = QuietestWorkerSpin(50, rounds, false); #ifdef _WIN32 constexpr const char* units = "cycles"; #else From 37c516ad2edc6d33f4369b7e306df34a35d41591 Mon Sep 17 00:00:00 2001 From: Adnan Khan Date: Sun, 25 Oct 2026 12:24:22 +0530 Subject: [PATCH 340/428] fix(relinker): keep the needed-module report best-effort on traversal and status errors --- core/relinker/cli/src/NativeModules.cpp | 19 +++++++++++++---- .../tests/test_guest_needed_modules.py | 21 +++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/core/relinker/cli/src/NativeModules.cpp b/core/relinker/cli/src/NativeModules.cpp index d10056fa28..6163063c69 100644 --- a/core/relinker/cli/src/NativeModules.cpp +++ b/core/relinker/cli/src/NativeModules.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include namespace Cli { @@ -155,16 +156,26 @@ void ReportUnconvertedNeeded(const NativeModules& modules, const std::vector found; - for (auto entry = std::filesystem::recursive_directory_iterator(modules.Root, std::filesystem::directory_options::skip_permission_denied); entry != std::filesystem::recursive_directory_iterator(); ++entry) { - if (entry->is_directory()) { - if (std::any_of(modules.Directories.begin(), modules.Directories.end(), [&](const auto& directory) { return SameDirectory(directory, entry->path()); })) entry.disable_recursion_pending(); + std::error_code error; + for (auto entry = std::filesystem::recursive_directory_iterator(modules.Root, std::filesystem::directory_options::skip_permission_denied, error); !error && entry != std::filesystem::recursive_directory_iterator(); entry.increment(error)) { + const bool directory = entry->is_directory(error); + if (error) { + error.clear(); + continue; + } + if (directory) { + if (std::any_of(modules.Directories.begin(), modules.Directories.end(), [&](const auto& scanned) { return std::filesystem::equivalent(scanned, entry->path(), error) && !error; })) entry.disable_recursion_pending(); + error.clear(); continue; } - if (!entry->is_regular_file()) continue; const auto filename = FoldedName(Io::Utf8Path(entry->path().filename()), foldCase); if (Domain::ModuleSuffix(filename).empty()) continue; const auto stem = stems.find(std::string(ModuleStem(filename))); if (stem == stems.end()) continue; + if (!entry->is_regular_file(error) || error) { + error.clear(); + continue; + } const auto header = TryReadCandidateHeader(entry->path()); if (!header || (!header->Self && !(header->Elf && header->Type == 0xfe18))) continue; for (const auto& name : stem->second) found.push_back({entry->path().lexically_relative(modules.Root), name, header->Self}); diff --git a/core/relinker/relinker/tests/test_guest_needed_modules.py b/core/relinker/relinker/tests/test_guest_needed_modules.py index c6b04179dc..4cd66ab6f5 100644 --- a/core/relinker/relinker/tests/test_guest_needed_modules.py +++ b/core/relinker/relinker/tests/test_guest_needed_modules.py @@ -162,6 +162,27 @@ def check_needed_outside(relinker, work): finally: (case / "needed.prx").chmod(0o644) + if os.name != "nt": + case = work / "outside-loops" + (case / "sce_module").mkdir(parents=True) + (case / "Media").mkdir() + (case / "Media" / "needed.prx").write_bytes(sce_module) + os.symlink("loop", case / "loop") + os.symlink("needed.prx", case / "needed.prx") + os.symlink("missing.prx", case / "Media" / "needed.sprx") + (case / "Media" / "Nested").mkdir() + os.symlink("..", case / "Media" / "Nested" / "needed.suprx") + expected = [message + "needed.prx at Media/needed.prx; add --module-dir Media"] + assert convert(case) == expected + if os.geteuid() != 0: + (case / "locked").mkdir() + (case / "locked" / "needed.prx").write_bytes(sce_module) + (case / "locked").chmod(0) + try: + assert convert(case) == expected + finally: + (case / "locked").chmod(0o755) + def main(): relinker = Path(sys.argv[1]).resolve() From 185098a39d9179907e11d15fa9a58b27e6aac306 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Fri, 23 Oct 2026 12:34:45 -0400 Subject: [PATCH 341/428] fix(libc): compute log1p with FreeBSD msun's algorithm, as titles' libc.prx does log1p returned the host's log1p, whose last bit differs from the titles' copy on Windows on 24,477,433 of 320 million random arguments. The titles' copy is FreeBSD msun's s_log1p.c, and it sets EDOM at msun's NaN return, for arguments below -1, -inf and negative NaNs, but not for log1p(-1). log1p now computes that algorithm and sets EDOM there. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Math.cpp | 90 ++++++++++++++++++++++++++++++++- core/libs/tests/GuestMath.cpp | 30 +++++++++++ docs/dev/debt/libc.md | 1 + 3 files changed, 120 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index b3b175de0c..e635b0b1e1 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -504,7 +504,95 @@ double APS5_VABI atan_nid_postfix(double x) { return std::atan(x); } double APS5_VABI tan_nid_postfix(double x) { return std::tan(x); } double APS5_VABI log2_nid_postfix(double x) { return std::log2(x); } double APS5_VABI log_nid_postfix(double x) { return std::log(x); } -double APS5_VABI log1p_nid_postfix(double x) { return std::log1p(x); } +double APS5_VABI log1p_nid_postfix(double x) { + const double ln2_hi = 6.93147180369123816490e-01; + const double ln2_lo = 1.90821492927058770002e-10; + const double two54 = 1.80143985094819840000e+16; + const double Lp1 = 6.666666666666735130e-01; + const double Lp2 = 3.999999999940941908e-01; + const double Lp3 = 2.857142874366239149e-01; + const double Lp4 = 2.222219843214978396e-01; + const double Lp5 = 1.818357216161805012e-01; + const double Lp6 = 1.531383769920937332e-01; + const double Lp7 = 1.479819860511658591e-01; + static volatile double vzero = 0.0; + const std::int32_t hx = static_cast(std::bit_cast(x) >> 32); + const std::int32_t ax = hx & 0x7fffffff; + std::int32_t k = 1; + std::int32_t hu = 0; + double f = 0.0; + double c = 0.0; + if (hx < 0x3fda827a) { + if (ax >= 0x3ff00000) { + if (x == -1.0) { + return -two54 / vzero; + } + errno = EDOM; + return (x - x) / (x - x); + } + if (ax < 0x3e200000) { + if (two54 + x > 0.0 && ax < 0x3c900000) { + return x; + } + return x - x * x * 0.5; + } + if (hx > 0 || hx <= static_cast(0xbfd2bec4)) { + k = 0; + f = x; + hu = 1; + } + } + if (hx >= 0x7ff00000) { + return x + x; + } + if (k != 0) { + double u; + if (hx < 0x43400000) { + u = 1.0 + x; + hu = static_cast(std::bit_cast(u) >> 32); + k = (hu >> 20) - 1023; + c = k > 0 ? 1.0 - (u - x) : x - (u - 1.0); + c /= u; + } else { + u = x; + hu = static_cast(std::bit_cast(u) >> 32); + k = (hu >> 20) - 1023; + c = 0; + } + hu &= 0x000fffff; + const std::uint64_t ulow = std::bit_cast(u) & 0xffffffffull; + if (hu < 0x6a09e) { + u = std::bit_cast((static_cast(static_cast(hu | 0x3ff00000)) << 32) | ulow); + } else { + k += 1; + u = std::bit_cast((static_cast(static_cast(hu | 0x3fe00000)) << 32) | ulow); + hu = (0x00100000 - hu) >> 2; + } + f = u - 1.0; + } + const double hfsq = 0.5 * f * f; + if (hu == 0) { + if (f == 0.0) { + if (k == 0) { + return 0.0; + } + c += k * ln2_lo; + return k * ln2_hi + c; + } + const double R = hfsq * (1.0 - 0.66666666666666666 * f); + if (k == 0) { + return f - R; + } + return k * ln2_hi - ((R - (k * ln2_lo + c)) - f); + } + const double s = f / (2.0 + f); + const double z = s * s; + const double R = z * (Lp1 + z * (Lp2 + z * (Lp3 + z * (Lp4 + z * (Lp5 + z * (Lp6 + z * Lp7)))))); + if (k == 0) { + return f - (hfsq - s * (hfsq + R)); + } + return k * ln2_hi - ((hfsq - (s * (hfsq + R) + (k * ln2_lo + c))) - f); +} float APS5_VABI log1pf_nid_postfix(float x) { return std::log1p(x); } static double Expm1(double x) { const double tiny = 1.0e-300; diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index f666ff8755..7c6bd1e44e 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -542,6 +542,35 @@ static void CheckExpm1ConsoleBits() { } } +static void CheckLog1pConsoleBits() { + constexpr struct { + std::uint64_t input; + std::uint64_t expected; + int error; + } cases[] = { + {0x0000000000000000ull, 0x0000000000000000ull, 0}, {0x8000000000000000ull, 0x8000000000000000ull, 0}, + {0x3c8fffffffffffffull, 0x3c8fffffffffffffull, 0}, {0x3c90000000000000ull, 0x3c90000000000000ull, 0}, + {0x3e1fffffffffffffull, 0x3e1fffffff7fffffull, 0}, {0x3e20000000000000ull, 0x3e1fffffff800000ull, 0}, + {0xbe20000000000000ull, 0xbe20000000400000ull, 0}, {0x3fda8279ffffffffull, 0x3fd62e43471c5d9dull, 0}, + {0x3fda827a00000000ull, 0x3fd62e43471c5d9eull, 0}, {0xbfd2bec400000000ull, 0xbfd62e4420e1ff10ull, 0}, + {0xbfd2bec3ffffffffull, 0xbfd62e4420e1ff0full, 0}, {0x3ff0000000000000ull, 0x3fe62e42fefa39efull, 0}, + {0xbfe0000000000000ull, 0xbfe62e42fefa39efull, 0}, {0xbfefffffffffffffull, 0xc0425e4f7b2737faull, 0}, + {0xbff0000000000000ull, 0xfff0000000000000ull, 0}, {0xbff0000000000001ull, 0xfff8000000000000ull, EDOM}, + {0xc000000000000000ull, 0xfff8000000000000ull, EDOM}, {0x433fffffffffffffull, 0x40425e4f7b2737faull, 0}, + {0x4340000000000000ull, 0x40425e4f7b2737faull, 0}, {0x7fefffffffffffffull, 0x40862e42fefa39efull, 0}, + {0x7ff0000000000000ull, 0x7ff0000000000000ull, 0}, {0xfff0000000000000ull, 0xfff8000000000000ull, EDOM}, + {0x7ff8000000000001ull, 0x7ff8000000000001ull, 0}, {0xfff8000000000002ull, 0xfff8000000000002ull, EDOM}, + {0x3f0f5ab6d3865ccaull, 0x3f0f5a7962a0216cull, 0}, {0x3fe8683f0ace287cull, 0x3fe223b9ede79242ull, 0}, + {0x3f5f8cd247124706ull, 0x3f5f850e039f6187ull, 0}, {0x40298faede87074bull, 0x4004fc71ddb00cc0ull, 0}, + {0x404b621796f08648ull, 0x401015adbb851c70ull, 0}, {0x3f21ecf9339d7420ull, 0x3f21eca8e0601790ull, 0}, + }; + for (const auto& item : cases) { + errno = 1234; + Require(std::bit_cast(log1p_nid_postfix(std::bit_cast(item.input))) == item.expected); + Require(errno == (item.error != 0 ? item.error : 1234)); + } +} + static void CheckAsinl() { const struct { std::uint64_t inputMantissa; std::uint16_t inputSignExponent; std::uint64_t outputMantissa; std::uint16_t outputSignExponent; } cases[] = { {0, 0x0000, 0, 0x0000}, {0, 0x8000, 0, 0x8000}, @@ -1576,6 +1605,7 @@ int main() { CheckLongDouble(); CheckNearbyintl(); CheckExpm1ConsoleBits(); + CheckLog1pConsoleBits(); CheckAsinl(); Require(scalbn_nid_postfix(0.75, -2) == 0.1875); Require(scalbnf_nid_postfix(0.75f, 4) == 12.f); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 62fa7183d8..9d0a63659d 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -107,6 +107,7 @@ - [lgamma and signgam](../../../core/libs/prx/libc/src/Math.cpp) (libc) - `lgamma` returns the host's `lgamma` and stores the sign of Γ(x) in the exported `signgam` (NID `5gOOC0kzW0c`, starting at 0 as in FreeBSD 11's [`s_signgam.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/s_signgam.c)) by the rules of [`e_lgamma_r.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/e_lgamma_r.c): -1 for -0 and for a negative non-integer whose floor is odd, 1 otherwise, including poles, infinities and NaN. Taken from the FreeBSD source, not measured on the console. The NID is in the aerolib symbol list; no title is known to read `signgam`. PPSA99004 imports `lgamma` - [coshf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows FreeBSD msun's [`e_coshf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_coshf.c), using the `expm1f` and `expf` paths already inlined for `sinhf` below; the Windows UCRT differed from the `libc.prx` copies in PPSA07923 and PPSA13934 on 500,554 and 500,614 of 20 million inputs in the #9707 survey. The exact system `libSceLibcInternal` behavior and the FMA-contracted `libc.prx` results remain unverified - [expm1](../../../core/libs/prx/libc/src/Math.cpp) (libc) does not compare floating-point exceptions, and its double inputs are checked by sampling, not exhaustively. It is FreeBSD msun's [`s_expm1.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_expm1.c) with `huge` read at run time, and sets `ERANGE` only when a finite argument overflows to infinity, as the titles' `libc.prx` does. It gives the same bits and `errno` as the `expm1` bundled with PPSA07923 and PPSA05916 on 320 million random and 47,222 structured arguments on a Windows host; the FMA-contracted build bundled with PPSA13934 differs on 43,433 of the random ones. +- [log1p](../../../core/libs/prx/libc/src/Math.cpp) (libc) does not compare floating-point exceptions, and its double inputs are checked by sampling, not exhaustively. It is FreeBSD msun's [`s_log1p.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_log1p.c). As the titles' `libc.prx` does, it sets `EDOM` at msun's NaN return (arguments below -1, `-inf` and negative NaNs) and leaves `errno` alone for `log1p(-1)`, where it returns `-inf`. It gives the same bits, NaN payloads included, and `errno` as the `log1p` bundled with PPSA07923 and PPSA05916 on 320 million random and 68,216 structured arguments on a Windows host. The FMA-contracted build bundled with PPSA13934 differs on 73,682 of the random ones. - [sinhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `sinhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_sinhf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_sinhf.c) from before `__ldexp_expf`, which squares `expf(|x| / 2)` between `logf(FLT_MAX)` and the overflow threshold, calling msun's [`expm1f`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_expm1f.c) and [`expf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_expf.c), whose paths for the arguments it passes are inlined here. Past the threshold Sony's error helper sets `errno` to `ERANGE` before returning `x * 1e37`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits as this implementation for every finite argument up to the threshold, of both signs; it was not run on Linux. That the system `libSceLibcInternal`, which Ruffle (PPSA68091) and RetroArch (PPSA99169) import while bundling a zero-return `libc.prx`, has the same code is assumed - [acoshf](../../../core/libs/prx/libc/src/Math.cpp) (libc) uses FreeBSD msun's [`e_acoshf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_acoshf.c) range handling; the Windows UCRT differed from the `libc.prx` copies in PPSA07923 and PPSA13934 on 216,017 and 216,024 of 20 million inputs in the #9707 survey. The invalid-domain path raises `FE_INVALID` without setting `errno` as the source does. The exact system `libSceLibcInternal` behavior and FMA-contracted results remain unverified - [acoshl, asinhl, atanhl, atan2l, cbrtl, erfl, erfcl, expm1l, hypotl, log1pl, log2l, lgammal, tgammal, j0f, j1f, jnf, y0f, y1f and ynf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return the host C library's results (the float Bessel functions through its double ones), not those of the console's own code, so the last bits can differ (`y0f`, `y1f`, `ynf` follow the console at `+-0`). As in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x56410), `lgammal` does not store the sign in `signgam`; `hypotl` returns infinity for an infinite argument even when the other is a NaN, as C11 F.10.4.3 requires. [hypot3, hypot3f and hypot3l](../../../core/libs/prx/libc/src/Math.cpp) follow that module (0x51D30, 0x520D0, 0x52420): the first NaN of x, y and z is returned unchanged, any infinity gives positive infinity, a zero argument gives the two-argument hypot of the other two, and otherwise the largest magnitude is combined with the hypot of the other two From 0c799de53858cd6b98b87842af191cbdf0ecff9c Mon Sep 17 00:00:00 2001 From: hypermist Date: Sun, 25 Oct 2026 19:57:38 +1300 Subject: [PATCH 342/428] test(libSceFont): stop pinning the unimplemented font-set outline profile path The font-set record path still throws in production, but the test no longer asserts it: tests cover implemented behaviour only. The null-output check on a font-set handle stays. The outline profile debt entry states the gap without naming a source. Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestFontOutlineProfile.cpp | 8 -------- docs/dev/debt/libSceFont.md | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/core/libs/tests/GuestFontOutlineProfile.cpp b/core/libs/tests/GuestFontOutlineProfile.cpp index 302739a8a8..1c57782d45 100644 --- a/core/libs/tests/GuestFontOutlineProfile.cpp +++ b/core/libs/tests/GuestFontOutlineProfile.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include static void Check(bool value, int line) { @@ -92,14 +91,7 @@ int main() { auto* nativeFont = reinterpret_cast(profiled); nativeFont->open_info.fontset_record = record.data(); Require(sceFontGetFontGlyphsOutlineProfile(profiled, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); - bool recordThrew = false; - try { - sceFontGetFontGlyphsOutlineProfile(profiled, &profile); - } catch (const std::runtime_error&) { - recordThrew = true; - } nativeFont->open_info.fontset_record = nullptr; - Require(recordThrew); Fill(profile); Require(sceFontGetFontGlyphsOutlineProfile(profiled, &profile) == SCE_FONT_OK && profile.maxPoints == 120); diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index 1e5aadf93d..33d22eb558 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -24,7 +24,7 @@ - [sceFontGetCharGlyphMetrics, sceFontGetRenderCharGlyphMetrics](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) report the vertical metrics the way the [FreeType driver](../../../core/libs/prx/libSceFontFt/src/Driver.cpp) reports them for a rendered glyph, not confirmed on a title: FreeType's vertical advance, and for a face without vertical metrics a bearing of half the horizontal advance left of the horizontal bearing and of half the advance left over by the glyph height. - [sceFontGetFontMetrics](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) - the 64-byte output, the form fallbacks (2 and 3 fall back to the `hhea` values when OS/2 is missing or gives an empty range, 4 builds vertical values from `vhea` or OS/2) and the raw weight deltas and slant copied from the style are not measured on a console. For a font set the values are those of its primary file; which file the console reports is not recovered. Closes when: measured on a PS5 - [sceFontGetFontStyleInformation](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) - the 24-byte output (FreeType family and style names, italic and bold flags, OS/2 weight class when it is a multiple of 100 from 100 to 900, width class below 10, and 1 plus a right-to-left `head` direction plus 0x80 for vertical metrics) and the font-set record path are not measured on a console. A face without a `head` table, such as a BDF font, throws, as its console result is unknown. Closes when: measured on a PS5 -- [sceFontGetFontGlyphsOutlineProfile](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) - the 16-byte output (the `maxp` maximum contours, points, composite contours and composite points) and `SCE_FONT_ERROR_NO_SUPPORT_FUNCTION` for a `maxp` older than 1.0 are read from the disassembly of firmware 13.60 `libSceFont.sprx` and `libSceFontFt.sprx`, not measured on a console. A handle with a font-set record throws: the console takes the maximum over the record's system font entries, which are not modelled. A face without a `maxp` table throws, as the console reads it unchecked. Closes when: measured on a PS5 +- [sceFontGetFontGlyphsOutlineProfile](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) - the 16-byte output (the `maxp` maximum contours, points, composite contours and composite points) and `SCE_FONT_ERROR_NO_SUPPORT_FUNCTION` for a `maxp` older than 1.0 are not measured on a console. A handle with a font-set record throws, as the maximum over the record's system font entries is not modelled. A face without a `maxp` table throws, as its console result is unknown. Closes when: measured on a PS5 - [Font writing lines](../../../core/libs/prx/libSceFont/src/WritingLine.cpp): visual ordering and spacing originated from PS4 measurements and match the supplied PS5 structure and control flow. Exhaustive floating-point equivalence, including NaN payloads and rounding, has not been verified against a running PS5 module. - [Font graphics and metric signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): graphics-device/service creation takes font memory, parameters and a handle output; destruction takes a handle pointer. Attribute selectors and outputs are 32-bit values. Expansion-buffer outputs remain opaque pending field recovery. Protected implementations are unchanged. From 2c5fcf6406b73004b51782dbea0a9c2dde9a8999 Mon Sep 17 00:00:00 2001 From: ali zedan Date: Thu, 22 Oct 2026 00:28:02 +0300 Subject: [PATCH 343/428] fix(kernel): safely copy guest path arguments --- core/libs/prx/libc/include/General.hpp | 1 + core/libs/prx/libc/src/General.cpp | 70 +++++++++++ core/libs/prx/libc/src/Rename.cpp | 13 +- core/libs/prx/libkernel/File/src/Open.cpp | 17 ++- .../prx/libkernel/File/src/SharedMemory.cpp | 6 + core/libs/prx/libkernel/File/src/Stdio.cpp | 85 +++++++++---- core/libs/prx/libkernel_sys/Export.cpp | 103 +++++++++------- core/libs/tests/GuestKernelSys.cpp | 116 ++++++++++++++++++ 8 files changed, 333 insertions(+), 78 deletions(-) diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index b45386ab48..6bdf4a76c6 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -27,6 +27,7 @@ extern "C" std::FILE* HostStandardErrorFile_nid_no_patch(); #endif extern "C" bool CopyGuestEnvironment_nid_no_patch(const char* name, std::string& value); +extern "C" int CopyGuestPath_nid_no_patch(const char* source, std::string& destination); extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path); extern "C" std::filesystem::path ResolvePathOrRelinkedModule_nid_no_patch(const char* path); diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 73c1a10082..fd15323585 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -1,5 +1,7 @@ #include "prx/libc/include/Api.hpp" +#include #include +#include #include #include #include @@ -8,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -29,12 +32,19 @@ #include #include #include +#ifdef __linux__ +#include +#elif defined(__APPLE__) +#include +#include +#endif #include #endif #include "prx/libc/include/General.hpp" #include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/AppArchive.hpp" #include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/GuestArena.hpp" namespace { // Guest prefixes (without leading slashes) mapped to host directories, e.g. save-data mount points: @@ -51,6 +61,41 @@ PathAliases& Aliases() { return *aliases; } +std::size_t HostPageSize() { + static const std::size_t size = [] { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + return static_cast(info.dwPageSize); +#else + const long value = sysconf(_SC_PAGESIZE); + if (value <= 0) throw std::runtime_error("cannot query the host page size"); + return static_cast(value); +#endif + }(); + return size; +} + +bool CopyGuestBytes(void* destination, const void* source, std::size_t bytes) { + GuestArena::GuestArenaPrepareHostRead_nid_no_patch(source, bytes); +#ifdef _WIN32 + SIZE_T copied = 0; + return ReadProcessMemory(GetCurrentProcess(), source, destination, bytes, &copied) != 0 && copied == bytes; +#elif defined(__linux__) + iovec local{destination, bytes}; + iovec remote{const_cast(source), bytes}; + const auto copied = process_vm_readv(getpid(), &local, 1, &remote, 1, 0); + if (copied < 0 && errno != EFAULT) throw std::runtime_error("guest path: process_vm_readv failed with errno " + std::to_string(errno)); + return copied >= 0 && static_cast(copied) == bytes; +#elif defined(__APPLE__) + mach_vm_size_t copied = 0; + return mach_vm_read_overwrite(mach_task_self(), reinterpret_cast(source), bytes, + reinterpret_cast(destination), &copied) == KERN_SUCCESS && copied == bytes; +#else + throw std::runtime_error("checked guest path copies are not implemented on this host"); +#endif +} + std::string TrimSlashes(const char* path) { std::string s(path); std::size_t start = 0; @@ -489,6 +534,31 @@ int SetWorkingDirectory(WorkingDirectory& state, const std::filesystem::path& re } } +extern "C" int CopyGuestPath_nid_no_patch(const char* source, std::string& destination) { + constexpr std::size_t maximum = 1024; + if (source == nullptr) return 14; + const auto address = reinterpret_cast(source); + std::array bytes{}; + std::size_t offset = 0; + while (offset < bytes.size()) { + if (address > std::numeric_limits::max() - offset) return 14; + const auto current = address + offset; + const auto pageRemaining = HostPageSize() - current % HostPageSize(); + const auto count = std::min(pageRemaining, bytes.size() - offset); + if (!CopyGuestBytes(bytes.data() + offset, reinterpret_cast(current), count)) return 14; + if (const auto* end = static_cast(std::memchr(bytes.data() + offset, 0, count))) { + try { + destination.assign(bytes.data(), static_cast(end - bytes.data())); + } catch (const std::bad_alloc&) { + return 12; + } + return 0; + } + offset += count; + } + return 63; +} + #ifndef _WIN32 namespace { std::mutex g_hostStandardErrorMutex; diff --git a/core/libs/prx/libc/src/Rename.cpp b/core/libs/prx/libc/src/Rename.cpp index 36454860ae..8c41ae277f 100644 --- a/core/libs/prx/libc/src/Rename.cpp +++ b/core/libs/prx/libc/src/Rename.cpp @@ -107,10 +107,17 @@ static int SourceLookupError_nid_no_patch(const std::filesystem::path& source) { } extern "C" int RenameGuestPath_nid_no_patch(const char* from, const char* to) { - if (!from) { errno = 14; return -1; } - if (!*from) { errno = 2; return -1; } + std::string fromCopy; + if (const int error = CopyGuestPath_nid_no_patch(from, fromCopy)) { errno = error; return -1; } + from = fromCopy.c_str(); + if (fromCopy.empty()) { errno = 2; return -1; } if (const int dot = RenameDotError_nid_no_patch(from)) { errno = dot; return -1; } - const int destinationError = !to ? 14 : !*to ? 2 : RenameDotError_nid_no_patch(to); + std::string toCopy; + int destinationError = CopyGuestPath_nid_no_patch(to, toCopy); + if (destinationError == 0) { + to = toCopy.c_str(); + destinationError = toCopy.empty() ? 2 : RenameDotError_nid_no_patch(to); + } try { const auto source = ResolvePath_nid_no_patch(from); if (destinationError) { diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 49bf78e813..751afaf175 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -262,7 +262,10 @@ extern "C" { int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { if (File::InvalidOpenFlags(flags)) return SCE_KERNEL_ERROR_EINVAL; - if (path != nullptr && *path == '\0') return SCE_KERNEL_ERROR_ENOENT; + std::string copy; + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceKernelError(error); + path = copy.c_str(); + if (copy.empty()) return SCE_KERNEL_ERROR_ENOENT; APS5_LOG_OUT("path=%s flags=0x%X nativeFlags=0x%X mode=0%o", path, flags, MapFlags(flags), mode); if (path != nullptr && File::IsRandomDevicePath(path)) { if ((flags & SCE_KERNEL_O_ACCMODE) != SCE_KERNEL_O_RDONLY) throw std::runtime_error(std::string(__func__) + ": writing to " + path + " is not implemented"); @@ -416,8 +419,10 @@ static int StatNative(const std::filesystem::path& native, bool directory, FileS } int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { - if (path == nullptr) return SceErrorFromErrno(GuestEfault); - if (*path == '\0') return SceErrorFromErrno(2); + std::string copy; + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceErrorFromErrno(error); + path = copy.c_str(); + if (copy.empty()) return SceErrorFromErrno(2); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native, true, true)) return SceKernelError(error); return StatNative(native, std::string_view(path).back() == '/', sb); @@ -435,8 +440,10 @@ int StatInDirectory_nid_no_patch(int directory, const char* path, FileStat* sb, } int APS5_VABI sceKernelUnlink(const char* path) { - if (path == nullptr) return SceErrorFromErrno(GuestEfault); - if (*path == '\0') return SceErrorFromErrno(2); + std::string copy; + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceErrorFromErrno(error); + path = copy.c_str(); + if (copy.empty()) return SceErrorFromErrno(2); if (const int dot = RenameDotError_nid_no_patch(path)) return SceKernelError(dot); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); diff --git a/core/libs/prx/libkernel/File/src/SharedMemory.cpp b/core/libs/prx/libkernel/File/src/SharedMemory.cpp index 67315a6478..8e18c4bcb6 100644 --- a/core/libs/prx/libkernel/File/src/SharedMemory.cpp +++ b/core/libs/prx/libkernel/File/src/SharedMemory.cpp @@ -992,6 +992,9 @@ extern "C" int APS5_VABI shm_open_nid_postfix(const char* path, int flags, int m if (accessMode != SCE_KERNEL_O_RDONLY && accessMode != SCE_KERNEL_O_RDWR) return Fail(GuestInvalid); if ((flags & ~AcceptedFlags) != 0) return Fail(GuestInvalid); if (path != anonymousObject) { + std::string copy; + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return Fail(error); + path = copy.c_str(); #ifdef _WIN32 return OpenNamedFile(path, flags, mode); #else @@ -1007,6 +1010,9 @@ extern "C" int APS5_VABI shm_open_nid_postfix(const char* path, int flags, int m } extern "C" int APS5_VABI shm_unlink_nid_postfix(const char* path) { + std::string copy; + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return Fail(error); + path = copy.c_str(); #ifdef _WIN32 return UnlinkNamedFile(path); #else diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 338dc6c3a8..3abccedbe6 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -597,9 +597,9 @@ extern "C" int APS5_VABI pipe_nid_postfix(int* descriptors) { return 0; } -static int PathError(const char* path) { - if (path == nullptr) return GUEST_EFAULT; - return *path == '\0' ? GUEST_ENOENT : 0; +static int PathError(const char* path, std::string& copy) { + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return error; + return copy.empty() ? GUEST_ENOENT : 0; } #ifdef _WIN32 @@ -682,7 +682,9 @@ static int SetFileFlags(const char* function, const std::string& target, std::ui extern "C" { int APS5_VABI chmod_nid_postfix(const char* path, int mode) { - if (const int error = PathError(path)) return PosixFailure(error); + std::string copy; + if (const int error = PathError(path, copy)) return PosixFailure(error); + path = copy.c_str(); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); if (NativeChmod(native, mode & 07777) != 0) return PosixResult(SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno)); @@ -690,7 +692,9 @@ int APS5_VABI chmod_nid_postfix(const char* path, int mode) { } int APS5_VABI chflags_nid_postfix(const char* path, std::uint64_t flags) { - if (const int error = PathError(path)) return PosixFailure(error); + std::string copy; + if (const int error = PathError(path, copy)) return PosixFailure(error); + path = copy.c_str(); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); if (const int error = LookupError(native)) return PosixFailure(error); @@ -943,7 +947,9 @@ int64_t APS5_VABI lseek_nid_postfix(int d, int64_t offset, int whence) { } int APS5_VABI mkdir_nid_postfix(const char* path, uint16_t mode) { - if (const int error = PathError(path)) return PosixFailure(error); + std::string copy; + if (const int error = PathError(path, copy)) return PosixFailure(error); + path = copy.c_str(); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); if (NativeMkdir(native, mode) != 0) { @@ -960,7 +966,9 @@ int APS5_VABI mkdir_nid_postfix(const char* path, uint16_t mode) { int APS5_VABI open_nid_postfix(const char* path, int flags, int mode) { if (File::InvalidOpenFlags(flags)) return PosixFailure(GUEST_EINVAL); - if (const int error = PathError(path)) return PosixFailure(error); + std::string copy; + if (const int error = PathError(path, copy)) return PosixFailure(error); + path = copy.c_str(); return PosixResult(sceKernelOpen(path, flags, static_cast(mode))); } @@ -1043,13 +1051,17 @@ std::int64_t APS5_VABI _write_nid_postfix(int descriptor, const void* buffer, st int APS5_VABI stat_nid_postfix(const char* path, FileStat* sb) { if (sb == nullptr) return PosixFailure(GUEST_EFAULT); - if (const int error = PathError(path)) return PosixFailure(error); + std::string copy; + if (const int error = PathError(path, copy)) return PosixFailure(error); + path = copy.c_str(); return PosixResult(sceKernelStat(path, sb)); } int APS5_VABI lstat_nid_postfix(const char* path, FileStat* sb) { if (sb == nullptr) return PosixFailure(GUEST_EFAULT); - if (const int error = PathError(path)) return PosixFailure(error); + std::string copy; + if (const int error = PathError(path, copy)) return PosixFailure(error); + path = copy.c_str(); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); if (!File::FillLinkStat(native, sb)) return PosixResult(SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno)); @@ -1057,13 +1069,18 @@ int APS5_VABI lstat_nid_postfix(const char* path, FileStat* sb) { } int APS5_VABI unlink_nid_postfix(const char* path) { - if (const int error = PathError(path)) return PosixFailure(error); + std::string copy; + if (const int error = PathError(path, copy)) return PosixFailure(error); + path = copy.c_str(); return PosixResult(sceKernelUnlink(path)); } int APS5_VABI sceKernelCheckReachability(const char* path) { if (path == nullptr) return SCE_KERNEL_ERROR_EINVAL; - if (*path == '\0') return SCE_KERNEL_ERROR_ENOENT; + std::string copy; + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceKernelError(error); + path = copy.c_str(); + if (copy.empty()) return SCE_KERNEL_ERROR_ENOENT; if (std::strlen(path) > 255) return SCE_KERNEL_ERROR_ENAMETOOLONG; std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native, true, true)) return SceKernelError(error); @@ -1238,8 +1255,10 @@ int APS5_VABI getdents_nid_postfix(int fd, char* buf, int nbytes) { int APS5_VABI sceKernelMkdir(const char* path, uint16_t mode) { (void)mode; - if (!path) return SceErrorFromErrno(GUEST_EFAULT); - if (!*path) return SceErrorFromErrno(GUEST_ENOENT); + std::string copy; + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceErrorFromErrno(error); + path = copy.c_str(); + if (copy.empty()) return SceErrorFromErrno(GUEST_ENOENT); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); while (!native.has_filename() && native.has_relative_path()) native = native.parent_path(); @@ -1504,8 +1523,10 @@ int APS5_VABI sceKernelRename(const char* from, const char* to) { } int APS5_VABI sceKernelRmdir(const char* path) { - if (!path) return SceErrorFromErrno(GUEST_EFAULT); - if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); + std::string copy; + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceErrorFromErrno(error); + path = copy.c_str(); + if (copy.empty()) return SceErrorFromErrno(GUEST_ENOENT); if (const int dot = RenameDotError_nid_no_patch(path)) return SceKernelError(dot); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); @@ -1526,7 +1547,9 @@ int APS5_VABI sceKernelRmdir(const char* path) { } int APS5_VABI rmdir_nid_postfix(const char* path) { - if (const int error = PathError(path)) return PosixFailure(error); + std::string copy; + if (const int error = PathError(path, copy)) return PosixFailure(error); + path = copy.c_str(); return PosixResult(sceKernelRmdir(path)); } @@ -1535,8 +1558,10 @@ int APS5_VABI rmdir_nid_postfix(const char* path) { extern "C" { int APS5_VABI sceKernelChmod_nid_postfix(const char* path, std::uint16_t mode) { - if (!path) return SceErrorFromErrno(GUEST_EFAULT); - if (!*path) return SceErrorFromErrno(GUEST_ENOENT); + std::string copy; + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceErrorFromErrno(error); + path = copy.c_str(); + if (copy.empty()) return SceErrorFromErrno(GUEST_ENOENT); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); if (NativeChmod(native, mode & 07777) != 0) return SceErrorFromErrno(errno); @@ -1564,8 +1589,10 @@ static bool InvalidTimes(const KernelTimeval* times) { int APS5_VABI sceKernelTruncate_nid_postfix(const char* path, std::int64_t length) { if (length < 0) return SceErrorFromErrno(GUEST_EINVAL); - if (!path) return SceErrorFromErrno(GUEST_EFAULT); - if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); + std::string copy; + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceErrorFromErrno(error); + path = copy.c_str(); + if (copy.empty()) return SceErrorFromErrno(GUEST_ENOENT); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); std::error_code error; @@ -1585,14 +1612,18 @@ int APS5_VABI sceKernelTruncate_nid_postfix(const char* path, std::int64_t lengt int APS5_VABI truncate_nid_postfix(const char* path, std::int64_t length) { if (length < 0) return PosixFailure(GUEST_EINVAL); - if (const int error = PathError(path)) return PosixFailure(error); + std::string copy; + if (const int error = PathError(path, copy)) return PosixFailure(error); + path = copy.c_str(); return PosixResult(sceKernelTruncate_nid_postfix(path, length)); } int APS5_VABI sceKernelUtimes_nid_postfix(const char* path, const KernelTimeval* times) { if (InvalidTimes(times)) return SCE_KERNEL_ERROR_EINVAL; - if (!path) return SceErrorFromErrno(GUEST_EFAULT); - if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); + std::string copy; + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return SceErrorFromErrno(error); + path = copy.c_str(); + if (copy.empty()) return SceErrorFromErrno(GUEST_ENOENT); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); if (NativeUtimes(native, times) != 0) return SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno); @@ -1601,13 +1632,17 @@ int APS5_VABI sceKernelUtimes_nid_postfix(const char* path, const KernelTimeval* int APS5_VABI utimes_nid_postfix(const char* path, const KernelTimeval* times) { if (InvalidTimes(times)) return PosixFailure(GUEST_EINVAL); - if (const int error = PathError(path)) return PosixFailure(error); + std::string copy; + if (const int error = PathError(path, copy)) return PosixFailure(error); + path = copy.c_str(); return PosixResult(sceKernelUtimes_nid_postfix(path, times)); } int APS5_VABI lutimes_nid_postfix(const char* path, const KernelTimeval* times) { if (InvalidTimes(times)) return PosixFailure(GUEST_EINVAL); - if (const int error = PathError(path)) return PosixFailure(error); + std::string copy; + if (const int error = PathError(path, copy)) return PosixFailure(error); + path = copy.c_str(); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); if (NativeUtimes(native, times, false) != 0) return PosixResult(SceErrorFromErrno(errno)); diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index ca040d4946..4125e11eea 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -162,6 +162,12 @@ std::int64_t PathConfigurationValue(int name, const char* function) { } } +int CopyPath(const char*& path, std::string& copy) { + if (const int error = CopyGuestPath_nid_no_patch(path, copy)) return error; + path = copy.c_str(); + return copy.empty() ? GuestEnoent : 0; +} + int GuestError(const std::error_code& error) { const auto condition = error.default_error_condition(); if (condition == std::errc::resource_unavailable_try_again) return GuestEagain; @@ -351,8 +357,8 @@ int APS5_VABI fstat_nid_postfix(int d, FileStat* sb) { std::int64_t APS5_VABI readlink_nid_postfix(const char* path, char* buffer, std::size_t size) { if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); - if (path == nullptr) return Fail(GuestEfault); - if (*path == '\0') return Fail(GuestEnoent); + std::string copy; + if (const int error = CopyPath(path, copy)) return Fail(error); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return Fail(error); return ReadNativeLink(native, buffer, size); @@ -360,8 +366,8 @@ std::int64_t APS5_VABI readlink_nid_postfix(const char* path, char* buffer, std: std::int64_t APS5_VABI readlinkat_nid_postfix(int fd, const char* path, char* buffer, std::size_t size) { if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); - if (path == nullptr) return Fail(GuestEfault); - if (*path == '\0') return Fail(GuestEnoent); + std::string copy; + if (const int error = CopyPath(path, copy)) return Fail(error); if (fd == GuestAtFdcwd || *path == '/') return readlink_nid_postfix(path, buffer, size); int error = 0; const char* base = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); @@ -371,8 +377,8 @@ std::int64_t APS5_VABI readlinkat_nid_postfix(int fd, const char* path, char* bu } std::int64_t APS5_VABI pathconf_nid_postfix(const char* path, int name) { - if (path == nullptr) return Fail(GuestEfault); - if (*path == '\0') return Fail(GuestEnoent); + std::string copy; + if (const int error = CopyPath(path, copy)) return Fail(error); if (std::strlen(path) >= GuestPathMax || HasLongComponent(path)) return Fail(GuestEnametoolong); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return Fail(error); @@ -463,12 +469,13 @@ static int LinkNative(const std::filesystem::path& source, const std::filesystem } int APS5_VABI link_nid_postfix(const char* existing, const char* created) { - if (existing == nullptr || created == nullptr) return static_cast(Fail(GuestEfault)); - if (*existing == '\0' || *created == '\0') return static_cast(Fail(GuestEnoent)); + std::string existingCopy, createdCopy; + if (const int error = CopyPath(existing, existingCopy)) return static_cast(Fail(error)); std::filesystem::path source, destination; if (const int error = ResolvePathError_nid_no_patch(existing, source)) return static_cast(Fail(error)); - if (const int error = ResolvePathError_nid_no_patch(created, destination)) return static_cast(Fail(error)); if (const int error = LinkSourceError(source)) return static_cast(Fail(error)); + if (const int error = CopyPath(created, createdCopy)) return static_cast(Fail(error)); + if (const int error = ResolvePathError_nid_no_patch(created, destination)) return static_cast(Fail(error)); return LinkNative(source, destination); } @@ -484,14 +491,13 @@ static int ResolveAtPath(int fd, const char* path, std::filesystem::path& native int APS5_VABI linkat_nid_postfix(int fd1, const char* path1, int fd2, const char* path2, int flag) { if (flag & ~GuestAtSymlinkFollow) return static_cast(Fail(GuestEinval)); - if (path1 == nullptr) return static_cast(Fail(GuestEfault)); - if (*path1 == '\0') return static_cast(Fail(GuestEnoent)); + std::string copy1, copy2; + if (const int error = CopyPath(path1, copy1)) return static_cast(Fail(error)); std::filesystem::path source, destination; if (const int error = ResolveAtPath(fd1, path1, source)) return static_cast(Fail(error)); if (flag == 0 && IsSymbolicLink(source)) NotImplemented_nid_no_patch("linkat: a hard link to a symbolic link itself"); if (const int error = LinkSourceError(source)) return static_cast(Fail(error)); - if (path2 == nullptr) return static_cast(Fail(GuestEfault)); - if (*path2 == '\0') return static_cast(Fail(GuestEnoent)); + if (const int error = CopyPath(path2, copy2)) return static_cast(Fail(error)); if (const int error = ResolveAtPath(fd2, path2, destination)) return static_cast(Fail(error)); return LinkNative(source, destination); } @@ -510,8 +516,8 @@ int APS5_VABI fchdir_nid_postfix(int fd) { } int APS5_VABI chroot_nid_postfix(const char* path) { - if (path == nullptr) return static_cast(Fail(GuestEfault)); - if (*path == '\0') return static_cast(Fail(GuestEnoent)); + std::string copy; + if (const int error = CopyPath(path, copy)) return static_cast(Fail(error)); if (std::strlen(path) >= GuestPathMax || HasLongComponent(path)) return static_cast(Fail(GuestEnametoolong)); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return static_cast(Fail(error)); @@ -527,9 +533,9 @@ int APS5_VABI chroot_nid_postfix(const char* path) { } int APS5_VABI symlink_nid_postfix(const char* target, const char* created) { - if (target == nullptr || created == nullptr) return static_cast(Fail(GuestEfault)); - if (std::strlen(target) >= GuestPathMax) return static_cast(Fail(GuestEnametoolong)); - if (*target == '\0' || *created == '\0') return static_cast(Fail(GuestEnoent)); + std::string targetCopy, createdCopy; + if (const int error = CopyPath(target, targetCopy)) return static_cast(Fail(error)); + if (const int error = CopyPath(created, createdCopy)) return static_cast(Fail(error)); const std::filesystem::path contents(target); if (contents.has_root_name() || contents.has_root_directory()) { NotImplemented_nid_no_patch("symlink: absolute targets"); @@ -588,8 +594,8 @@ int APS5_VABI openat_nid_postfix(int fd, const char* path, int flags, ...) { #endif } if (File::InvalidOpenFlags(flags)) return static_cast(Fail(GuestEinval)); - if (path == nullptr) return static_cast(Fail(GuestEfault)); - if (*path == '\0') return static_cast(Fail(GuestEnoent)); + std::string copy; + if (const int error = CopyPath(path, copy)) return static_cast(Fail(error)); if (fd == GuestAtFdcwd || *path == '/') return open_nid_postfix(path, flags, mode); const int result = OpenInDirectory_nid_no_patch(fd, path, flags, static_cast(mode)); if (result < 0) return static_cast(Fail(result & 0xffff)); @@ -616,8 +622,8 @@ int APS5_VABI _openat_nid_postfix(int fd, const char* path, int flags, ...) { int APS5_VABI mkdirat_nid_postfix(int fd, const char* path, std::uint16_t mode) { - if (path == nullptr) return static_cast(Fail(GuestEfault)); - if (*path == '\0') return static_cast(Fail(GuestEnoent)); + std::string copy; + if (const int error = CopyPath(path, copy)) return static_cast(Fail(error)); if (fd == GuestAtFdcwd || *path == '/') return mkdir_nid_postfix(path, mode); if (std::strlen(path) >= GuestPathMax || HasLongComponent(path)) return static_cast(Fail(GuestEnametoolong)); int error = 0; @@ -653,8 +659,8 @@ int APS5_VABI mkdirat_nid_postfix(int fd, const char* path, std::uint16_t mode) int APS5_VABI fchmodat_nid_postfix(int fd, const char* path, int mode, int flag) { if (flag != 0 && flag != GuestAtSymlinkNofollow) return static_cast(Fail(GuestEinval)); - if (path == nullptr) return static_cast(Fail(GuestEfault)); - if (*path == '\0') return static_cast(Fail(GuestEnoent)); + std::string copy; + if (const int error = CopyPath(path, copy)) return static_cast(Fail(error)); std::filesystem::path native; if (fd == GuestAtFdcwd || *path == '/') { if (const int error = ResolvePathError_nid_no_patch(path, native)) return static_cast(Fail(error)); @@ -692,9 +698,13 @@ int APS5_VABI fchmodat_nid_postfix(int fd, const char* path, int mode, int flag) } int APS5_VABI lchmod_nid_postfix(const char* path, std::uint16_t mode) { + std::string copy; + if (path != nullptr) { + if (const int error = CopyPath(path, copy)) return static_cast(Fail(error)); + } #ifndef __APPLE__ std::filesystem::path native; - if (path != nullptr && *path != '\0' && ResolvePathError_nid_no_patch(path, native) == 0) { + if (path != nullptr && ResolvePathError_nid_no_patch(path, native) == 0) { #ifdef _WIN32 const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); const bool symbolic = attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0; @@ -755,8 +765,8 @@ int APS5_VABI fchownat_nid_postfix(int fd, const char* path, std::uint32_t uid, int APS5_VABI fstatat_nid_postfix(int fd, const char* path, FileStat* sb, int flag) { if (flag & ~GuestAtSymlinkNofollow) return static_cast(Fail(GuestEinval)); - if (path == nullptr) return static_cast(Fail(GuestEfault)); - if (*path == '\0') return static_cast(Fail(GuestEnoent)); + std::string copy; + if (const int error = CopyPath(path, copy)) return static_cast(Fail(error)); if (fd == GuestAtFdcwd || *path == '/') return flag ? lstat_nid_postfix(path, sb) : stat_nid_postfix(path, sb); const int result = StatInDirectory_nid_no_patch(fd, path, sb, flag == 0); if (result < 0) return static_cast(Fail(result & 0xffff)); @@ -765,8 +775,8 @@ int APS5_VABI fstatat_nid_postfix(int fd, const char* path, FileStat* sb, int fl int APS5_VABI unlinkat_nid_postfix(int fd, const char* path, int flag) { if (flag & ~GuestAtRemovedir) return static_cast(Fail(GuestEinval)); - if (path == nullptr) return static_cast(Fail(GuestEfault)); - if (*path == '\0') return static_cast(Fail(GuestEnoent)); + std::string copy; + if (const int error = CopyPath(path, copy)) return static_cast(Fail(error)); if (fd == GuestAtFdcwd || *path == '/') return (flag & GuestAtRemovedir) ? rmdir_nid_postfix(path) : unlink_nid_postfix(path); if (std::strlen(path) >= GuestPathMax || HasLongComponent(path)) return static_cast(Fail(GuestEnametoolong)); int error = 0; @@ -842,14 +852,13 @@ int APS5_VABI scePthreadCondattrGetclock(const PthreadCondattr* attr, KernelCloc } int APS5_VABI renameat_nid_postfix(int oldfd, const char* from, int newfd, const char* to) { - if (from == nullptr || to == nullptr) return static_cast(Fail(GuestEfault)); - if (*from == '\0' || *to == '\0') return static_cast(Fail(GuestEnoent)); - if ((oldfd == GuestAtFdcwd || *from == '/') && (newfd == GuestAtFdcwd || *to == '/')) + std::string fromCopy, toCopy; + if (const int error = CopyPath(from, fromCopy)) return static_cast(Fail(error)); + if ((oldfd == GuestAtFdcwd || *from == '/') && newfd == GuestAtFdcwd) return RenameGuestPath_nid_no_patch(from, to); - if (std::strlen(from) >= GuestPathMax || HasLongComponent(from) || - std::strlen(to) >= GuestPathMax || HasLongComponent(to)) + if (std::strlen(from) >= GuestPathMax || HasLongComponent(from)) return static_cast(Fail(GuestEnametoolong)); - if (IsDotOrDotDot(from) || IsDotOrDotDot(to)) return static_cast(Fail(GuestEinval)); + if (IsDotOrDotDot(from)) return static_cast(Fail(GuestEinval)); std::filesystem::path source; if (oldfd == GuestAtFdcwd || *from == '/') { @@ -861,6 +870,20 @@ int APS5_VABI renameat_nid_postfix(int oldfd, const char* from, int newfd, const source = std::filesystem::path(reinterpret_cast(base)) / std::filesystem::path(reinterpret_cast(from)); } + const std::string_view fromView(from); + const bool oldTrailingSlash = !fromView.empty() && fromView.back() == '/'; + while (!source.has_filename() && source.has_relative_path()) source = source.parent_path(); + std::error_code statusError; + const auto sourceStatus = std::filesystem::symlink_status(source, statusError); + if (statusError && statusError != std::errc::no_such_file_or_directory) + return static_cast(Fail(FilesystemError(statusError))); + if (sourceStatus.type() == std::filesystem::file_type::not_found) + return static_cast(Fail(HasNonDirectoryPrefix(source) ? GuestEnotdir : GuestEnoent)); + if (oldTrailingSlash && !std::filesystem::is_directory(sourceStatus)) return static_cast(Fail(GuestEnotdir)); + + if (const int error = CopyPath(to, toCopy)) return static_cast(Fail(error)); + if (std::strlen(to) >= GuestPathMax || HasLongComponent(to)) return static_cast(Fail(GuestEnametoolong)); + if (IsDotOrDotDot(to)) return static_cast(Fail(GuestEinval)); std::filesystem::path destination; if (newfd == GuestAtFdcwd || *to == '/') { @@ -873,21 +896,11 @@ int APS5_VABI renameat_nid_postfix(int oldfd, const char* from, int newfd, const std::filesystem::path(reinterpret_cast(to)); } - const std::string_view fromView(from); const std::string_view toView(to); - const bool oldTrailingSlash = !fromView.empty() && fromView.back() == '/'; const bool newTrailingSlash = !toView.empty() && toView.back() == '/'; - while (!source.has_filename() && source.has_relative_path()) source = source.parent_path(); while (!destination.has_filename() && destination.has_relative_path()) destination = destination.parent_path(); - std::error_code statusError; - const auto sourceStatus = std::filesystem::symlink_status(source, statusError); - if (oldTrailingSlash) { - if (sourceStatus.type() == std::filesystem::file_type::not_found) - return static_cast(Fail(HasNonDirectoryPrefix(source) ? GuestEnotdir : GuestEnoent)); - if (!std::filesystem::is_directory(sourceStatus)) return static_cast(Fail(GuestEnotdir)); - } if (newTrailingSlash && sourceStatus.type() != std::filesystem::file_type::not_found && !std::filesystem::is_directory(sourceStatus)) return static_cast(Fail(GuestEnotdir)); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index f8b47c0b67..81997c91e1 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -5,6 +5,7 @@ #include "prx/libc/include/Api.hpp" #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/General.hpp" #include "prx/libc/include/RemoveTree.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" #include "prx/libkernel/KernelErrors.hpp" @@ -30,6 +31,7 @@ #endif #ifndef _WIN32 #include +#include #include #endif @@ -181,6 +183,119 @@ bool RemoveSymlink(const std::string& link) { #endif } +class GuardedPaths { +public: + GuardedPaths() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + page = info.dwPageSize; + memory = static_cast(VirtualAlloc(nullptr, 2 * page, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(memory != nullptr); + DWORD previous = 0; + Require(VirtualProtect(memory + page, page, PAGE_NOACCESS, &previous) != 0); +#else + const long size = sysconf(_SC_PAGESIZE); + Require(size > 0); + page = static_cast(size); + memory = static_cast(mmap(nullptr, 2 * page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(memory != MAP_FAILED); + Require(mprotect(memory + page, page, PROT_NONE) == 0); +#endif + } + + ~GuardedPaths() { +#ifdef _WIN32 + Require(VirtualFree(memory, 0, MEM_RELEASE) != 0); +#else + Require(munmap(memory, 2 * page) == 0); +#endif + } + + char* Unreadable() const { return memory + page; } + char* Crossing() const { return memory + page - 3; } + char* Boundary() const { return memory + page - 2; } + +private: + char* memory = nullptr; + std::size_t page = 0; +}; + +void CheckUnreadablePaths(const std::string& renameSource, int directory) { + constexpr int Fault = 14; + constexpr int KernelFault = static_cast(0x8002000Eu); + GuardedPaths paths; + std::memcpy(paths.Crossing(), "abc", 3); + std::string copy; + Require(CopyGuestPath_nid_no_patch(paths.Unreadable(), copy) == Fault); + Require(CopyGuestPath_nid_no_patch(paths.Crossing(), copy) == Fault); + paths.Boundary()[0] = '.'; + paths.Boundary()[1] = '\0'; + Require(CopyGuestPath_nid_no_patch(paths.Boundary(), copy) == 0 && copy == "."); + const std::string maximum(1023, 'p'); + std::array unterminated{}; + unterminated.fill('p'); + Require(CopyGuestPath_nid_no_patch(maximum.c_str(), copy) == 0 && copy == maximum); + Require(CopyGuestPath_nid_no_patch(unterminated.data(), copy) == 63); + const auto posixFault = [&](auto call) { + *__error_nid_postfix() = 0; + Require(call() == -1 && Error() == Fault); + }; + FileStat status{}; + char linkBuffer[16]{}; + posixFault([&] { return open_nid_postfix(paths.Unreadable(), SCE_KERNEL_O_RDONLY, 0); }); + posixFault([&] { return chmod_nid_postfix(paths.Unreadable(), 0); }); + posixFault([&] { return mkdir_nid_postfix(paths.Unreadable(), 0755); }); + posixFault([&] { return rmdir_nid_postfix(paths.Unreadable()); }); + posixFault([&] { return unlink_nid_postfix(paths.Unreadable()); }); + posixFault([&] { return stat_nid_postfix(paths.Unreadable(), &status); }); + posixFault([&] { return lstat_nid_postfix(paths.Unreadable(), &status); }); + posixFault([&] { return truncate_nid_postfix(paths.Unreadable(), 0); }); + posixFault([&] { return utimes_nid_postfix(paths.Unreadable(), nullptr); }); + posixFault([&] { return lutimes_nid_postfix(paths.Unreadable(), nullptr); }); + posixFault([&] { return readlink_nid_postfix(paths.Unreadable(), linkBuffer, sizeof(linkBuffer)); }); + posixFault([&] { return pathconf_nid_postfix(paths.Unreadable(), 4); }); + posixFault([&] { return link_nid_postfix(paths.Unreadable(), "unused-link"); }); + posixFault([&] { return link_nid_postfix(renameSource.c_str(), paths.Unreadable()); }); + posixFault([&] { return symlink_nid_postfix(paths.Unreadable(), "unused-symlink"); }); + posixFault([&] { return symlink_nid_postfix("target", paths.Unreadable()); }); + posixFault([&] { return openat_nid_postfix(-100, paths.Unreadable(), SCE_KERNEL_O_RDONLY); }); + posixFault([&] { return mkdirat_nid_postfix(-100, paths.Unreadable(), 0755); }); + posixFault([&] { return fchmodat_nid_postfix(-100, paths.Unreadable(), 0, 0); }); + posixFault([&] { return lchmod_nid_postfix(paths.Unreadable(), 0); }); + posixFault([&] { return fstatat_nid_postfix(-100, paths.Unreadable(), &status, 0); }); + posixFault([&] { return unlinkat_nid_postfix(-100, paths.Unreadable(), 0); }); + posixFault([&] { return readlinkat_nid_postfix(-100, paths.Unreadable(), linkBuffer, sizeof(linkBuffer)); }); + posixFault([&] { return renameat_nid_postfix(-100, paths.Unreadable(), -100, "unused-rename"); }); + posixFault([&] { return renameat_nid_postfix(-100, renameSource.c_str(), -100, paths.Unreadable()); }); + posixFault([&] { return openat_nid_postfix(directory, paths.Unreadable(), SCE_KERNEL_O_RDONLY); }); + posixFault([&] { return mkdirat_nid_postfix(directory, paths.Unreadable(), 0755); }); + posixFault([&] { return fchmodat_nid_postfix(directory, paths.Unreadable(), 0, 0); }); + posixFault([&] { return fstatat_nid_postfix(directory, paths.Unreadable(), &status, 0); }); + posixFault([&] { return unlinkat_nid_postfix(directory, paths.Unreadable(), 0); }); + posixFault([&] { return readlinkat_nid_postfix(directory, paths.Unreadable(), linkBuffer, sizeof(linkBuffer)); }); + posixFault([&] { return renameat_nid_postfix(directory, paths.Unreadable(), directory, "unused-rename"); }); + posixFault([&] { return renameat_nid_postfix(directory, "file.txt", directory, paths.Unreadable()); }); + *__error_nid_postfix() = 0; + Require(renameat_nid_postfix(directory, "missing", directory, paths.Unreadable()) == -1 && Error() == 2); + *__error_nid_postfix() = 0; + Require(link_nid_postfix("missing", paths.Unreadable()) == -1 && Error() == 2); + posixFault([&] { return shm_open_nid_postfix(paths.Unreadable(), SCE_KERNEL_O_RDWR, 0600); }); + posixFault([&] { return shm_unlink_nid_postfix(paths.Unreadable()); }); + Require(sceKernelOpen(paths.Unreadable(), SCE_KERNEL_O_RDONLY, 0) == KernelFault); + Require(sceKernelStat(paths.Unreadable(), &status) == KernelFault); + Require(sceKernelMkdir(paths.Unreadable(), 0755) == KernelFault); + Require(sceKernelRmdir(paths.Unreadable()) == KernelFault); + Require(sceKernelUnlink(paths.Unreadable()) == KernelFault); + Require(sceKernelChmod_nid_postfix(paths.Unreadable(), 0) == KernelFault); + Require(sceKernelTruncate_nid_postfix(paths.Unreadable(), 0) == KernelFault); + Require(sceKernelUtimes_nid_postfix(paths.Unreadable(), nullptr) == KernelFault); + Require(sceKernelCheckReachability(paths.Unreadable()) == KernelFault); + Require(sceKernelRename(paths.Unreadable(), ".") == KernelFault); + Require(sceKernelRename(renameSource.c_str(), paths.Unreadable()) == KernelFault); + Require(!std::filesystem::exists("unused-link") && !std::filesystem::exists("unused-symlink") && !std::filesystem::exists("unused-rename")); +} + void CheckSymlinkTargetRestrictions(const std::filesystem::path& root) { const auto created = (root / "rejected").string(); for (const auto& target : {std::string(".."), std::string("../x"), std::string("a/../b"), std::string("a/../../x"), std::string("/abs")}) @@ -737,6 +852,7 @@ int main() { CheckSymlinkTargetRestrictions(root); const int base = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); Require(base >= 0); + CheckUnreadablePaths(file, base); const int relative = openat_nid_postfix(base, "file.txt", SCE_KERNEL_O_RDONLY); char byte = 0; Require(relative >= 0 && sceKernelRead(relative, &byte, 1) == 1 && byte == 'x'); From 7535f2bb50ed7691e41c071daa10e14ddf5d0644 Mon Sep 17 00:00:00 2001 From: ali zedan Date: Thu, 22 Oct 2026 16:57:39 +0300 Subject: [PATCH 344/428] test(agc): open storage output before guarding reads --- .../prx/libSceAgcDriver/tests/execution/StorageFileWrite.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StorageFileWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageFileWrite.cpp index 5b9fbc5ade..8658f5b45b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StorageFileWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageFileWrite.cpp @@ -308,8 +308,8 @@ void CheckShutdownAndGuard() { OutputFile file; std::vector source(TransferBytes, std::byte{0x5a}); AgcDriver::GuestMemory::SetFlushHook(nullptr); - AgcDriver::GuestMemory::SetReadGuard([](std::uint64_t, std::size_t) { throw std::runtime_error("file source read guard"); }); const auto descriptor = file.Open(); + AgcDriver::GuestMemory::SetReadGuard([](std::uint64_t, std::size_t) { throw std::runtime_error("file source read guard"); }); bool refused = false; try { sceKernelWrite(descriptor, source.data(), source.size()); } catch (const std::runtime_error& error) { refused = std::string(error.what()) == "file source read guard"; } From 83043eede62bdde9cdea89fdff026d61a0584450 Mon Sep 17 00:00:00 2001 From: hypermist Date: Sun, 25 Oct 2026 20:40:44 +1300 Subject: [PATCH 345/428] test(libSceVrTracker2): stop asserting that system modes throw Remove the system-mode throw cases in GuestVrTracker2.cpp that tested modes 1000 and 1001 throwing. Allocate a fresh temporary directory atomically in EnterApplicationRoot without clobbering existing paths. Update the debt entry without citing a source. Assisted-by: Antigravity:gemini-3.8-flash-medium --- core/libs/tests/GuestVrTracker2.cpp | 15 +++++++-------- docs/dev/debt/libSceVrTracker2.md | 4 ++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/core/libs/tests/GuestVrTracker2.cpp b/core/libs/tests/GuestVrTracker2.cpp index 5985981b38..a1e4b0693a 100644 --- a/core/libs/tests/GuestVrTracker2.cpp +++ b/core/libs/tests/GuestVrTracker2.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -93,12 +94,6 @@ void CheckInitializeValidation(std::vector& memory) { parameter.mode = mode; Require(InitializeFresh(memory, parameter) == InvalidArgument, "initialize mode"); } - for (const std::uint32_t mode : {1000u, 1001u}) { - auto parameter = valid; - parameter.mode = mode; - parameter.workMemory = nullptr; - Require(Throws([&] { sceVrTracker2Initialize(¶meter); }), "initialize system mode category"); - } auto parameter = valid; parameter.workMemory = nullptr; Require(InitializeFresh(memory, parameter) == InvalidArgument, "initialize work memory null"); @@ -160,8 +155,12 @@ void CheckInitializeTracking(std::vector& memory, std::uint32_t sd } std::filesystem::path EnterApplicationRoot() { - const auto root = std::filesystem::temp_directory_path() / ("guest_vr_tracker2_" + std::to_string(getpid_nid_postfix())); - std::filesystem::remove_all(root); + std::random_device device; + std::filesystem::path root; + do { + root = std::filesystem::temp_directory_path() / + ("guest_vr_tracker2_" + std::to_string(getpid_nid_postfix()) + "_" + std::to_string(device())); + } while (!std::filesystem::create_directory(root)); std::filesystem::create_directories(root / "app0" / "sce_sys"); std::ofstream(root / "app0" / "sce_sys" / "param.json") << "{\n \"contentId\": \"IV0000-ABCD00000_00-VRTRACKER2000000\",\n \"localizedParameters\": {\"defaultLanguage\": \"en-US\", \"en-US\": {\"titleName\": \"VrTracker2\"}},\n \"titleId\": \"ABCD00000\"\n}\n"; std::filesystem::current_path(root); diff --git a/docs/dev/debt/libSceVrTracker2.md b/docs/dev/debt/libSceVrTracker2.md index c7c0424754..5bb664cbf1 100644 --- a/docs/dev/debt/libSceVrTracker2.md +++ b/docs/dev/debt/libSceVrTracker2.md @@ -2,11 +2,11 @@ ### Silent stubs -- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) never initializes; initialized tracking behavior is absent. Its uninitialized responses were measured on firmware 13.60 without a headset: most exports return 0x8A740001 without writes, while specific output/reset helpers clear buffers or write defaults. These helpers still lack complete signatures. Named export signatures and play-area output layouts are recovered; initialization option semantics and result fields need reconstruction. Anonymous exports remain grouped by their observed response rather than their individual contracts. +- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) never initializes; initialized tracking behavior is absent. Its uninitialized responses were measured without a headset: most exports return 0x8A740001 without writes, while specific output/reset helpers clear buffers or write defaults. These helpers still lack complete signatures. Named export signatures and play-area output layouts are recovered; initialization option semantics and result fields need reconstruction. Anonymous exports remain grouped by their observed response rather than their individual contracts. ### Unknown function info -- [sceVrTracker2Initialize](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - never initializes: after validation it returns the first non-zero libSceHmd2 tracking or LED-detector open/start result and resets to uninitialized, and throws if all four succeed or for modes 1000/1001, whose application category the host lacks. Unverified: the mode 0 category query is assumed to succeed, the `sceKernelGetAppInfo2` attribute checks are skipped (the host has no such record; a zeroed one passes), the daemon IPC client and work-memory heap are not created, rollback only undoes what the call opened, and the meanings of the options at 0x30, 0x38 and 0x39 remain unknown (0x39 is only range-checked). Firmware 13.60 libSceVrTracker2.sprx, read from disassembly, not measured on a console; the -1 a no-headset host gets relies on libSceHmd2's measured pre-initialization answers. PPSA05666 calls it with no headset and, with #7463, #7690 and #7658 applied, stopped on the NotImplemented. +- [sceVrTracker2Initialize](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - never initializes: after validation it returns the first non-zero libSceHmd2 tracking or LED-detector open/start result and resets to uninitialized, and throws if all four succeed or for modes 1000/1001, whose application category the host lacks. Unverified: the mode 0 category query is assumed to succeed, the `sceKernelGetAppInfo2` attribute checks are skipped (the host has no such record; a zeroed one passes), the daemon IPC client and work-memory heap are not created, rollback only undoes what the call opened, and the meanings of the options at 0x30, 0x38 and 0x39 remain unknown (0x39 is only range-checked). The behaviour is not measured on a console; the -1 a no-headset host gets relies on libSceHmd2's measured pre-initialization answers. PPSA05666 calls it with no headset and, with #7463, #7690 and #7658 applied, stopped on the NotImplemented. - [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - the seven anonymous throwing exports now have their consumed argument slots restored. Calibration output (`jRmuXjsknsI`) uses a 32-bit capacity with 608/1216-byte variants; its fields remain unresolved. Mesh requirements (`NBhWy+Gl0tY`) contain four 32-bit counts/sizes. The mode/buffer/capacity/size export (`9R6W33Au4B4`) uses 64-bit sizes. The 32/48-byte result parameters are recovered; result output and pose parameters still need full layouts; the telemetry input (`oZDm1bynCaA`) spans 7024 bytes. Their bodies remain unimplemented. - [Anonymous tracking queries](../../../core/libs/prx/libSceVrTracker2/Export.cpp) still lack initialized behavior. The paired option setter/getter consume 28/40-byte records with three integer options, a reset byte and one/two float vectors; option meanings remain unresolved. The scalar query uses an 8-byte size/float record. Diagnostic output spans 4096 bytes, extended output 0xD02E8 bytes, and tracking configuration input 1024 bytes; their fields remain unresolved. The relative-pose query uses two coordinate-system selectors and a 64-byte output containing a pose and 32 cleared bytes; the counter query uses a 64-bit output and a 32-bit selector whose meaning remains unresolved. Remaining aliases still need individual signatures. From 6f78759a6e7a48040029af56fc2a5c9af4179814 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 02:28:34 +0000 Subject: [PATCH 346/428] fix(libc): let sceLibcInternalHeapErrorReportForGame skip a closed standard output The heap error report threw FileStream: closed stream out of the export when the guest had closed its standard output, so a heap-error diagnostic aborted through the exception instead of reporting. The standard output part is now skipped when the stream is closed; the host diagnostic on the host stderr is still written. --- core/libs/prx/libc/src/HeapDiagnostics.cpp | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libc/src/HeapDiagnostics.cpp b/core/libs/prx/libc/src/HeapDiagnostics.cpp index 626f036e4f..15f420f9c7 100644 --- a/core/libs/prx/libc/src/HeapDiagnostics.cpp +++ b/core/libs/prx/libc/src/HeapDiagnostics.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/Api.hpp" +#include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/HeapDiagnostics.hpp" #include @@ -77,13 +78,15 @@ void APS5_VABI sceLibcInternalHeapErrorReportForGame_nid_postfix(void* heap, voi const auto* state = static_cast(heap); std::uint32_t number = 0; std::memcpy(&number, state + MspaceNumberOffset, sizeof(number)); - std::FILE* output = GuestStandardOutputFile_nid_no_patch(); - std::fprintf(output, "[SceLibc] A heap error is detected.\n[SceLibc] -- Mspace Name --\n[SceLibc] %s", state + MspaceNameOffset); - if (number != 0) std::fprintf(output, " (%2u)", number); - std::fputc('\n', output); - if (error == SegmentOutOfRange) std::fprintf(output, "[SceLibc] -- Error reason --\n[SceLibc] segment out of range.\n"); - std::fprintf(output, "[SceLibc] pid:<0x%lx> tid:<0x%lx>\n", CurrentProcessId(), CurrentThreadId()); - std::fflush(output); + if (_Stdout_nid_postfix.IsOpen()) { + std::FILE* output = GuestStandardOutputFile_nid_no_patch(); + std::fprintf(output, "[SceLibc] A heap error is detected.\n[SceLibc] -- Mspace Name --\n[SceLibc] %s", state + MspaceNameOffset); + if (number != 0) std::fprintf(output, " (%2u)", number); + std::fputc('\n', output); + if (error == SegmentOutOfRange) std::fprintf(output, "[SceLibc] -- Error reason --\n[SceLibc] segment out of range.\n"); + std::fprintf(output, "[SceLibc] pid:<0x%lx> tid:<0x%lx>\n", CurrentProcessId(), CurrentThreadId()); + std::fflush(output); + } std::fprintf(stderr, "[libc] heap error 0x%x: heap %p, block %p, reported from guest 0x%llx\n", error, heap, block, static_cast(reinterpret_cast(__builtin_return_address(0)))); } From c5798f8e7a02296ca0ca6481ceae0f3b0cb638f3 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 02:28:34 +0000 Subject: [PATCH 347/428] test(libc): report a heap error with the standard output closed After the formatted report, the guest standard output is closed and the report is requested again; it must return without throwing. The case fails on current main with the closed-stream exception. --- core/libs/tests/GuestHeapReportForGame.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestHeapReportForGame.cpp b/core/libs/tests/GuestHeapReportForGame.cpp index db56fe1a2c..ccc355a05d 100644 --- a/core/libs/tests/GuestHeapReportForGame.cpp +++ b/core/libs/tests/GuestHeapReportForGame.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/Api.hpp" +#include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -58,10 +59,20 @@ int main() { threw = true; } Require(threw, "null heap accepted"); + std::fflush(stdout); + const bool closed = _Stdout_nid_postfix.Close() == 0; + bool closedThrew = false; + try { + sceLibcInternalHeapErrorReportForGame_nid_postfix(heap, &block, 1); + } catch (const std::exception&) { + closedThrew = true; + } + if (!closed || closedThrew) { + std::fputs("heap error report on a closed standard output\n", stderr); + std::abort(); + } std::fflush(stderr); std::fclose(stderr); - std::fflush(stdout); - std::fclose(stdout); const std::string report = ReadAll(path); const std::string output = ReadAll(outputPath); From 5daac82d3218141151a3fafdc2413a053a280454 Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Fri, 23 Oct 2026 02:13:15 -0300 Subject: [PATCH 348/428] fix(agc): create tessellation pipelines without the pipeline cache on MoltenVK MoltenVK runs the tessellation evaluation stage as a post-tessellation vertex function that reads the control stage's per-vertex outputs from a raw buffer, and pads the evaluation input struct to every output of the control shader it is linked with. The shader library cache of a VkPipelineCache matches a cached conversion when the new pipeline has the inputs that conversion used, so an evaluation shader converted next to a hull with one 16-byte output is reused next to the merged hull, whose LDS record adds a second per-vertex output. The domain stage then reads the merged hull's control points at a 16-byte stride over a 48-byte buffer and the patch lands elsewhere. Created without the pipeline cache, each tessellation pipeline converts its stages for its own interface. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 9 ++++++ .../include/TessellationPipelineCache.hpp | 14 +++++++++ .../Execution/src/VulkanDevice.cpp | 4 +++ .../Graphics/include/Context.hpp | 1 + .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 7 +++-- .../tests/TessellationPipelineCache.cpp | 31 +++++++++++++++++++ 6 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/TessellationPipelineCache.hpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/TessellationPipelineCache.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 8e9843d48f..776999ca12 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -860,6 +860,15 @@ if(BUILD_TESTING) add_test(NAME agc_fragment_barycentric COMMAND agc_fragment_barycentric_tests) endif() +add_test_executable(agc_tessellation_pipeline_cache_tests EXCLUDE_FROM_ALL tests/TessellationPipelineCache.cpp) +target_include_directories(agc_tessellation_pipeline_cache_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +set_target_properties(agc_tessellation_pipeline_cache_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_tessellation_pipeline_cache_tests) +if(BUILD_TESTING) + set_target_properties(agc_tessellation_pipeline_cache_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_tessellation_pipeline_cache COMMAND agc_tessellation_pipeline_cache_tests) +endif() + add_test_executable(agc_float32_flush_control_tests EXCLUDE_FROM_ALL tests/Float32FlushControl.cpp) target_include_directories(agc_float32_flush_control_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) set_target_properties(agc_float32_flush_control_tests PROPERTIES CXX_EXTENSIONS OFF) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/TessellationPipelineCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/TessellationPipelineCache.hpp new file mode 100644 index 0000000000..7605cd2c68 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/TessellationPipelineCache.hpp @@ -0,0 +1,14 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_TESSELLATIONPIPELINECACHE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_TESSELLATIONPIPELINECACHE_HPP + +#include + +namespace AgcDriver { + +inline bool TessellationPipelineCacheUsable(VkDriverId driver) { + return driver != VK_DRIVER_ID_MOLTENVK; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 70fdef29e3..5c57ee31bd 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -20,6 +20,7 @@ #include "prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp" #include "prx/libSceAgcDriver/Execution/include/DrawInputsInPlace.hpp" #include "prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp" +#include "prx/libSceAgcDriver/Execution/include/TessellationPipelineCache.hpp" #include "prx/libSceAgcDriver/Execution/include/SubgroupClock.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentMode.hpp" @@ -351,6 +352,7 @@ struct VulkanDevice::State { VkDriverId driverId = VK_DRIVER_ID_MAX_ENUM; std::uint32_t pixelFunctionStorageDwords = 4096u; bool individuallyFreeDescriptorSets = false; + bool tessellationPipelineCache = true; PFN_vkGetCalibratedTimestampsKHR calibratedTimestamps = nullptr; PFN_vkCmdSetCheckpointNV checkpointMarker = nullptr; PFN_vkCmdWriteBufferMarkerAMD bufferMarker = nullptr; @@ -1275,6 +1277,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->driverId = driver.driverID; state->individuallyFreeDescriptorSets = driver.driverID == VK_DRIVER_ID_MESA_LLVMPIPE; state->fusedExactArithmetic = driver.driverID == VK_DRIVER_ID_MOLTENVK; + state->tessellationPipelineCache = TessellationPipelineCacheUsable(driver.driverID); state->subgroupControlBarriers = SubgroupControlBarrierUsable(driver.driverID); state->workgroupScopeFences = WorkgroupScopeFenceUsable(driver.driverID); } @@ -3955,6 +3958,7 @@ Graphics::Context VulkanDevice::buildContext() const { }; context.pipelineCacheSynchronization = state->pipelineCache ? &state->pipelineCache->Synchronization() : nullptr; context.individuallyFreeDescriptorSets = state->individuallyFreeDescriptorSets; + context.tessellationPipelineCache = state->tessellationPipelineCache; context.dmaBufImport = state->dmaBufImport; context.sparseBinding = state->sparseBinding; context.sparseResidencyBuffer = state->sparseResidencyBuffer; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 7e0e07d07b..dc2e00475a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -240,6 +240,7 @@ struct Context { std::uint32_t imageTableCapacity = 0; std::uint32_t samplerTableCapacity = 0; bool individuallyFreeDescriptorSets = false; + bool tessellationPipelineCache = true; bool imageTableUpdateAfterBind = false; bool samplerTableUpdateAfterBind = false; bool geometryShader = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index fcad4ff7bf..df4487e101 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -723,20 +723,21 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput prebuilt = true; return; } + const VkPipelineCache pipelineCache = state.stages.tessellation && !context.tessellationPipelineCache ? VK_NULL_HANDLE : context.pipelineCache; if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, false, _modules); else { auto pipelineCacheLock = context.LockPipelineCacheForUse(); - Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines"); + Check(context.Function("vkCreateGraphicsPipelines")(context.device, pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines"); } if (alternateAlpha) { blend.pAttachments = secondChannels.data(); auto pipelineCacheLock = context.LockPipelineCacheForUse(); - Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &secondChannel), "vkCreateGraphicsPipelines"); + Check(context.Function("vkCreateGraphicsPipelines")(context.device, pipelineCache, 1, &pipelineInfo, nullptr, &secondChannel), "vkCreateGraphicsPipelines"); } if (state.depthBiasPerFace) { raster.cullMode = VK_CULL_MODE_FRONT_BIT; auto pipelineCacheLock = context.LockPipelineCacheForUse(); - Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &backFaces), "vkCreateGraphicsPipelines"); + Check(context.Function("vkCreateGraphicsPipelines")(context.device, pipelineCache, 1, &pipelineInfo, nullptr, &backFaces), "vkCreateGraphicsPipelines"); } for (const auto module : _modules) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); _modules.clear(); diff --git a/core/libs/prx/libSceAgcDriver/tests/TessellationPipelineCache.cpp b/core/libs/prx/libSceAgcDriver/tests/TessellationPipelineCache.cpp new file mode 100644 index 0000000000..6c922e0bc9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/TessellationPipelineCache.cpp @@ -0,0 +1,31 @@ +#include "prx/libSceAgcDriver/Execution/include/TessellationPipelineCache.hpp" +#include + +namespace { + +struct Case { + VkDriverId driver; + bool usable; +}; + +constexpr Case Cases[]{ + {VK_DRIVER_ID_MOLTENVK, false}, + {VK_DRIVER_ID_MESA_RADV, true}, + {VK_DRIVER_ID_MESA_LLVMPIPE, true}, + {VK_DRIVER_ID_NVIDIA_PROPRIETARY, true}, + {VK_DRIVER_ID_AMD_PROPRIETARY, true}, + {VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS, true}, +}; + +} + +int main() { + int failures = 0; + for (const auto& item : Cases) { + if (AgcDriver::TessellationPipelineCacheUsable(item.driver) != item.usable) { + std::fprintf(stderr, "driver %d: expected tessellation pipelines %s the pipeline cache\n", static_cast(item.driver), item.usable ? "to use" : "not to use"); + ++failures; + } + } + return failures == 0 ? 0 : 1; +} From 2fd07f181dd7c6b87c9c2912d3059d6325c82c45 Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Fri, 23 Oct 2026 03:19:15 -0300 Subject: [PATCH 349/428] fix(agc): keep tessellation pipelines off pipeline libraries without the pipeline cache --- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 2 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index df4487e101..8ce59dade1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -321,7 +321,7 @@ void appendKey(std::vector& key, const TValue& value) { bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& state, const VertexInputLayout& input, const PipelineSetLayouts& resources, std::span shaders) { using Stage = ShaderRecompiler::ShaderStage; - if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList || BarycentricPatches(state, shaders) || SplitsAlternateAlpha(state) || std::ranges::any_of(shaders, [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->functionLdsBuffer; }) || FragmentInterlock(shaders)) return false; + if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || (state.stages.tessellation && !context.tessellationPipelineCache) || state.depthBiasPerFace || state.rectList || BarycentricPatches(state, shaders) || SplitsAlternateAlpha(state) || std::ranges::any_of(shaders, [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->functionLdsBuffer; }) || FragmentInterlock(shaders)) return false; keys = {}; for (const auto& shader : shaders) { const auto identity = shader.program->Identity(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 798fa5e3bb..ea891b199f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -8076,6 +8076,38 @@ void pipelineLibraryClearWaitsForBuildTests() { Require(mock.live == 0, "clearing during a library build leaked Vulkan objects"); } +void tessellationPipelineLibraryCacheTests() { + using namespace AgcDriver::Graphics; + for (const auto shared : {true, false}) { + mock = MockVulkan{}; + auto context = mockContext(); + context.graphicsPipelineLibrary = true; + context.tessellationShader = true; + context.tessellationPipelineCache = shared; + context.limits.maxTessellationPatchSize = 32; + context.limits.maxPushConstantsSize = 256; + context.limits.framebufferNoAttachmentsSampleCounts = VK_SAMPLE_COUNT_1_BIT; + State state{}; + state.stages.path = ShaderPath::Tessellation; + state.stages.tessellation = ShaderRecompiler::TessellationConfiguration{3, 3, 2, 1, 4, 128, 16, false}; + ShaderRecompiler::RecompileResult vertex, control, evaluation, fragment; + vertex.spirv = makeModule({}); + control.spirv = makeModule({}); + evaluation.spirv = makeModule({}); + fragment.spirv = makeModule({.fragment = true}); + vertex.variantId = 201; + control.variantId = 202; + evaluation.variantId = 203; + fragment.variantId = 204; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::TessellationControl, &control, 0}, {ShaderRecompiler::ShaderStage::TessellationEvaluation, &evaluation, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}}; + std::array mainKey{31}; + const PipelineSetLayouts resources{makeHandle(), VK_NULL_HANDLE, false, mainKey, {}}; + Require(Pipeline::Prebuild(context, state, VertexInputLayout{}, resources, shaders) == shared, shared ? "tessellation pipelines skipped graphics pipeline libraries with a shared pipeline cache" : "tessellation pipelines without the pipeline cache were built from graphics pipeline libraries"); + ClearPipelineLibraries(context.device); + Require(mock.live == 0, "tessellation pipeline library tests leaked Vulkan objects"); + } +} + void framebufferOwnerTests() { using namespace AgcDriver::Graphics; mock = MockVulkan{}; @@ -12070,6 +12102,7 @@ int main(int argc, char** argv) { retainedPixelStageTests(); pipelineLibrarySetTests(); pipelineLibraryClearWaitsForBuildTests(); + tessellationPipelineLibraryCacheTests(); framebufferOwnerTests(); hdrComponentSwapTests(); depthMaintenanceTests(); From c315203faddaa8783946b111ebb7a320a0b02cb9 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sun, 25 Oct 2026 21:06:20 +1300 Subject: [PATCH 350/428] test(savedata): assert the runtime_error thrown for null delete inputs --- core/libs/tests/GuestSaveDataDelete.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestSaveDataDelete.cpp b/core/libs/tests/GuestSaveDataDelete.cpp index b8eae7ef5c..9eea5603c3 100644 --- a/core/libs/tests/GuestSaveDataDelete.cpp +++ b/core/libs/tests/GuestSaveDataDelete.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,15 @@ void Check(bool condition, const std::string& what) { ++failures; } +bool Throws(const SaveDataDelete* del) { + try { + sceSaveDataDelete(del); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + int Delete(const char* data, std::size_t size) { SceSaveDataDirName name{}; std::memcpy(name.data, data, size); @@ -130,9 +140,9 @@ int main() { Check(sceSaveDataUmount2(0, &mountResult.mount_point) == 0, "the mounted save unmounts"); } - Check(sceSaveDataDelete(nullptr) == SaveDataErrorParameter, "null delete struct is rejected"); + Check(Throws(nullptr), "null delete struct throws"); SaveDataDelete nullDir{}; - Check(sceSaveDataDelete(&nullDir) == SaveDataErrorParameter, "null dir_name pointer is rejected"); + Check(Throws(&nullDir), "null dir_name pointer throws"); Check(Delete("kept", 5) == 0, "a valid name is deleted"); Check(!std::filesystem::exists(kept.parent_path()), "the valid save directory is gone"); Check(std::filesystem::exists(victim), "the directory beside the save root is untouched"); From 16996b5e64df4d8551f20b60aa9c8501d02325ba Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sun, 25 Oct 2026 22:31:59 +1300 Subject: [PATCH 351/428] revert: ci(windows): keep test temporary files on the runner's work disk (#10643) This reverts commit da6c581a02dbd3983ccc70fff92169acce802f77, reversing changes made to 10a41a0d501d78bdf6e6d84ca577284d713dbac1. On D:, the runner does not create 8.3 short names, so guest_named_shm_windows_short_temp and guest_named_shm_windows_long_short_temp skip and check_skips fails the Windows job. --- .github/workflows/build.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fd6f959726..7813e6b53e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -683,9 +683,6 @@ jobs: shell: cmd run: cmake --build build --target libs - shell: cmd - env: - TEMP: ${{ runner.temp }} - TMP: ${{ runner.temp }} run: python tools/select_tests.py ctest build --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log --output-junit ctest.xml - run: python tools/check_skips.py build/ctest.xml --job windows - name: Write the test graph of main From fb99d0bf8c2cb1e52650bda0eda22bfbaa143a48 Mon Sep 17 00:00:00 2001 From: anizayazcodpro-png Date: Sun, 25 Oct 2026 15:34:06 +0530 Subject: [PATCH 352/428] feat(libc): implement iswascii Implement iswascii in libc, checking whether a wide character falls within the 7-bit ASCII range (0x00 to 0x7F). Add unit test coverage in GuestRuneLocale. --- core/libs/prx/libc/include/Api.hpp | 1 + core/libs/prx/libc/src/RuneLocale.cpp | 1 + core/libs/tests/GuestRuneLocale.cpp | 5 ++++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index 8b08397c5e..45e36c4f89 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -782,6 +782,7 @@ int APS5_VABI iswpunct_nid_postfix(std::uint32_t c); int APS5_VABI iswspace_nid_postfix(std::uint32_t c); int APS5_VABI iswupper_nid_postfix(std::uint32_t c); int APS5_VABI iswxdigit_nid_postfix(std::uint32_t c); +int APS5_VABI iswascii_nid_postfix(std::uint32_t c); int APS5_VABI iswctype_nid_postfix(std::uint32_t c, std::uint64_t mask); int APS5_VABI _Iswctype_nid_postfix(std::uint32_t c, std::uint64_t type); std::uint64_t APS5_VABI wctype_nid_postfix(const char* property); diff --git a/core/libs/prx/libc/src/RuneLocale.cpp b/core/libs/prx/libc/src/RuneLocale.cpp index 18e5aceb1d..f038c5cd42 100644 --- a/core/libs/prx/libc/src/RuneLocale.cpp +++ b/core/libs/prx/libc/src/RuneLocale.cpp @@ -110,6 +110,7 @@ int APS5_VABI iswpunct_nid_postfix(std::uint32_t c) { return GuestRune::Is(c, Gu int APS5_VABI iswspace_nid_postfix(std::uint32_t c) { return GuestRune::Is(c, GuestRune::Space); } int APS5_VABI iswupper_nid_postfix(std::uint32_t c) { return GuestRune::Is(c, GuestRune::Upper); } int APS5_VABI iswxdigit_nid_postfix(std::uint32_t c) { return GuestRune::Is(c, GuestRune::Hex); } +int APS5_VABI iswascii_nid_postfix(std::uint32_t c) { return (c & ~0x7Fu) == 0; } int APS5_VABI iswctype_nid_postfix(std::uint32_t c, std::uint64_t mask) { return GuestRune::Is(c, mask); } int APS5_VABI _Iswctype_nid_postfix(std::uint32_t c, std::uint64_t type) { diff --git a/core/libs/tests/GuestRuneLocale.cpp b/core/libs/tests/GuestRuneLocale.cpp index b05986b76d..31a506c9f8 100644 --- a/core/libs/tests/GuestRuneLocale.cpp +++ b/core/libs/tests/GuestRuneLocale.cpp @@ -12,7 +12,6 @@ extern "C" { extern const unsigned char _DefaultRuneLocale_nid_postfix[4224]; extern const unsigned char* _CurrentRuneLocale_nid_postfix; extern int __mb_sb_limit_nid_postfix; - } void Require(bool condition, std::source_location location = std::source_location::current()) { @@ -85,6 +84,10 @@ int main() { } Require(entry.wide(0xffffffff) == 0 && iswctype_nid_postfix(0xffffffff, entry.mask) == 0); } + for (std::uint32_t c = 0; c < 300; ++c) { + Require((iswascii_nid_postfix(c) != 0) == (c <= 0x7f)); + } + Require(iswascii_nid_postfix(0xffffffff) == 0); Require(wctype_nid_postfix("ideogram") == 0x80000 && wctype_nid_postfix("special") == 0x100000); Require(wctype_nid_postfix("phonogram") == 0x200000 && wctype_nid_postfix("number") == 0x400000); Require(wctype_nid_postfix("rune") == 0xffffff00 && wctype_nid_postfix("") == 0 && wctype_nid_postfix("Alpha") == 0); From 0390178ba06a360f4c4799725bf0699e3a3c4916 Mon Sep 17 00:00:00 2001 From: atharvjairath <54663702+atharvjairath@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:34:03 +0530 Subject: [PATCH 353/428] feat(libressl): implement cipher metadata queries --- core/libs/prx/libSceLibreSsl/CMakeLists.txt | 15 +++ core/libs/prx/libSceLibreSsl/Export.cpp | 15 --- core/libs/prx/libSceLibreSsl/include/Api.hpp | 6 +- core/libs/prx/libSceLibreSsl/src/Cipher.cpp | 60 ++++++++++++ core/libs/tests/GuestLibreSslCipher.cpp | 97 ++++++++++++++++++++ docs/dev/debt/libSceLibreSsl.md | 4 +- 6 files changed, 178 insertions(+), 19 deletions(-) create mode 100644 core/libs/prx/libSceLibreSsl/src/Cipher.cpp create mode 100644 core/libs/tests/GuestLibreSslCipher.cpp diff --git a/core/libs/prx/libSceLibreSsl/CMakeLists.txt b/core/libs/prx/libSceLibreSsl/CMakeLists.txt index 9612c0369f..20e7b85ba9 100644 --- a/core/libs/prx/libSceLibreSsl/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl/CMakeLists.txt @@ -78,3 +78,18 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +target_sources(libSceLibreSsl PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/Cipher.cpp) + +if(BUILD_TESTING) + add_test_executable(guest_libressl_cipher_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslCipher.cpp) + target_include_directories(guest_libressl_cipher_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_cipher_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_cipher_tests) + add_test(NAME guest_libressl_cipher COMMAND guest_libressl_cipher_tests) + set_tests_properties(guest_libressl_cipher PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_cipher PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 894b49bff5..dfbd9c146d 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -983,21 +983,6 @@ int APS5_VABI RSA_verify_PKCS1_PSS_nid_postfix(void) { return 0; } -int APS5_VABI SSL_CIPHER_get_bits_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SSL_CIPHER_get_name_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SSL_CIPHER_get_version_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI SSL_CTX_add_client_CA_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl/include/Api.hpp b/core/libs/prx/libSceLibreSsl/include/Api.hpp index b23740b20a..a9bfae70a0 100644 --- a/core/libs/prx/libSceLibreSsl/include/Api.hpp +++ b/core/libs/prx/libSceLibreSsl/include/Api.hpp @@ -253,9 +253,9 @@ int APS5_VABI RSA_sign_nid_postfix(void); int APS5_VABI RSA_size_nid_postfix(void); int APS5_VABI RSA_verify_nid_postfix(void); int APS5_VABI RSA_verify_PKCS1_PSS_nid_postfix(void); -int APS5_VABI SSL_CIPHER_get_bits_nid_postfix(void); -int APS5_VABI SSL_CIPHER_get_name_nid_postfix(void); -int APS5_VABI SSL_CIPHER_get_version_nid_postfix(void); +int APS5_VABI SSL_CIPHER_get_bits_nid_postfix(const void* cipher, std::int32_t* algorithmBits); +const char* APS5_VABI SSL_CIPHER_get_name_nid_postfix(const void* cipher); +char* APS5_VABI SSL_CIPHER_get_version_nid_postfix(const void* cipher); int APS5_VABI SSL_CTX_add_client_CA_nid_postfix(void); int APS5_VABI SSL_CTX_callback_ctrl_nid_postfix(void); int APS5_VABI SSL_CTX_check_private_key_nid_postfix(void); diff --git a/core/libs/prx/libSceLibreSsl/src/Cipher.cpp b/core/libs/prx/libSceLibreSsl/src/Cipher.cpp new file mode 100644 index 0000000000..bf213e3cbd --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/Cipher.cpp @@ -0,0 +1,60 @@ +#include +#include +#include +#include "prx/libSceLibreSsl/libSceLibreSsl.hpp" + +namespace { + +struct Cipher { + std::int32_t valid; + const char* name; + std::uint64_t id; + std::uint64_t keyExchange; + std::uint64_t authentication; + std::uint64_t encryption; + std::uint64_t mac; + std::uint64_t protocol; + std::uint64_t strengthFlags; + std::uint64_t extraFlags; + std::int32_t strengthBits; + std::int32_t algorithmBits; +}; + +static_assert(sizeof(Cipher) == 88); +static_assert(offsetof(Cipher, name) == 8); +static_assert(offsetof(Cipher, id) == 16); +static_assert(offsetof(Cipher, strengthBits) == 80); +static_assert(offsetof(Cipher, algorithmBits) == 84); + +} // namespace + +extern "C" { + +int APS5_VABI SSL_CIPHER_get_bits_nid_postfix(const void* cipher, std::int32_t* algorithmBits) { + if (!cipher) return 0; + const auto* bytes = static_cast(cipher); + if (algorithmBits) { + std::int32_t bits; + std::memcpy(&bits, bytes + offsetof(Cipher, algorithmBits), sizeof(bits)); + *algorithmBits = bits; + } + std::int32_t strength; + std::memcpy(&strength, bytes + offsetof(Cipher, strengthBits), sizeof(strength)); + return strength; +} + +const char* APS5_VABI SSL_CIPHER_get_name_nid_postfix(const void* cipher) { + if (!cipher) return "(NONE)"; + const char* name; + std::memcpy(&name, static_cast(cipher) + offsetof(Cipher, name), sizeof(name)); + return name; +} + +char* APS5_VABI SSL_CIPHER_get_version_nid_postfix(const void* cipher) { + if (!cipher) return const_cast("(NONE)"); + std::uint64_t id; + std::memcpy(&id, static_cast(cipher) + offsetof(Cipher, id), sizeof(id)); + return const_cast((id >> 24) == 3 ? "TLSv1/SSLv3" : "unknown"); +} + +} diff --git a/core/libs/tests/GuestLibreSslCipher.cpp b/core/libs/tests/GuestLibreSslCipher.cpp new file mode 100644 index 0000000000..366264cc99 --- /dev/null +++ b/core/libs/tests/GuestLibreSslCipher.cpp @@ -0,0 +1,97 @@ +#include "prx/libSceLibreSsl/libSceLibreSsl.hpp" +#include +#include +#include +#include +#include +#include +#include + +struct alignas(8) RawCipher { + std::array prefix; + std::int32_t strength; + std::int32_t algorithm; +}; + +static_assert(sizeof(RawCipher) == 88); +static_assert(offsetof(RawCipher, strength) == 80); +static_assert(offsetof(RawCipher, algorithm) == 84); + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "failed: %s\n", message); + std::abort(); +} + +static void RequireString(const char* actual, const char* expected) { + Require(actual && std::strcmp(actual, expected) == 0, expected); +} + +int main() { + struct { std::int32_t before, bits, after; } output {0x12345678, -777, 0x23456789}; + Require(SSL_CIPHER_get_bits_nid_postfix(nullptr, &output.bits) == 0, "null cipher has zero strength"); + Require(output.bits == -777, "null cipher leaves the optional output untouched"); + Require(SSL_CIPHER_get_bits_nid_postfix(nullptr, nullptr) == 0, "both pointers may be null"); + RequireString(SSL_CIPHER_get_name_nid_postfix(nullptr), "(NONE)"); + RequireString(SSL_CIPHER_get_version_nid_postfix(nullptr), "(NONE)"); + + struct { std::uint64_t before; RawCipher cipher; std::uint64_t after; } guarded; + std::memset(&guarded, 0xa5, sizeof(guarded)); + RawCipher& cipher = guarded.cipher; + const char customName[] = "arbitrary caller-provided cipher"; + const char* name = customName; + std::memcpy(cipher.prefix.data() + 8, &name, sizeof(name)); + Require(SSL_CIPHER_get_name_nid_postfix(&cipher) == name, "name pointer is returned without copying or a registry lookup"); + name = nullptr; + std::memcpy(cipher.prefix.data() + 8, &name, sizeof(name)); + Require(SSL_CIPHER_get_name_nid_postfix(&cipher) == nullptr, "null name field is preserved"); + name = customName; + std::memcpy(cipher.prefix.data() + 8, &name, sizeof(name)); + + constexpr std::int32_t bitPairs[][2] { + {0, 0}, {40, 128}, {56, 64}, {112, 168}, {128, 128}, {192, 192}, {256, 256}, + {-1, -2}, {std::numeric_limits::min(), std::numeric_limits::max()}, + {std::numeric_limits::max(), std::numeric_limits::min()}, + }; + for (const auto& pair : bitPairs) { + cipher.strength = pair[0]; + cipher.algorithm = pair[1]; + const auto snapshot = guarded; + output.bits = -777; + Require(SSL_CIPHER_get_bits_nid_postfix(&cipher, &output.bits) == pair[0], "effective strength is distinct from algorithm bits"); + Require(output.bits == pair[1], "algorithm bits use the 32-bit field at guest offset 84"); + Require(output.before == 0x12345678 && output.after == 0x23456789, "output writes exactly four bytes"); + Require(SSL_CIPHER_get_bits_nid_postfix(&cipher, nullptr) == pair[0], "optional output may be omitted"); + Require(std::memcmp(&guarded, &snapshot, sizeof(guarded)) == 0, "queries preserve the cipher and its guards"); + } + + constexpr std::uint64_t lowIds[] {0, 1, 0x35, 0xc02f, 0xffffff}; + for (std::uint64_t version = 0; version < 256; ++version) { + for (const auto low : lowIds) { + const std::uint64_t id = (version << 24) | low; + std::memcpy(cipher.prefix.data() + 16, &id, sizeof(id)); + RequireString(SSL_CIPHER_get_version_nid_postfix(&cipher), version == 3 ? "TLSv1/SSLv3" : "unknown"); + } + } + constexpr std::uint64_t wideIds[] {0x103000035ULL, 0x800000000300c02fULL, 0xffffffff03000000ULL, + std::numeric_limits::max()}; + for (const auto id : wideIds) { + std::memcpy(cipher.prefix.data() + 16, &id, sizeof(id)); + RequireString(SSL_CIPHER_get_version_nid_postfix(&cipher), "unknown"); + } + + cipher.strength = 40; + cipher.algorithm = 128; + Require(SSL_CIPHER_get_bits_nid_postfix(&cipher, &cipher.algorithm) == 40, "output may alias the algorithm field"); + Require(cipher.algorithm == 128, "self-alias preserves algorithm bits"); + Require(SSL_CIPHER_get_bits_nid_postfix(&cipher, &cipher.strength) == 128, "algorithm output is written before strength is read"); + Require(cipher.strength == 128, "aliased output receives algorithm bits"); + + const auto snapshot = guarded; + const std::int32_t valid = 0; + std::memcpy(cipher.prefix.data(), &valid, sizeof(valid)); + Require(SSL_CIPHER_get_name_nid_postfix(&cipher) == customName, "valid flag does not gate name access"); + Require(SSL_CIPHER_get_bits_nid_postfix(&cipher, nullptr) == 128, "valid flag does not gate strength access"); + Require(guarded.before == snapshot.before && guarded.after == snapshot.after, "input guards remain intact"); + return 0; +} diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index bbc0e9fb0e..a9e5271b10 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -2,7 +2,7 @@ ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 348 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 345 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. - [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); the module's LibreSSL version is unknown and nothing is checked on the console. Behaviour that differs between LibreSSL versions throws: `SHA256` with a null digest buffer (older versions return a static buffer) and `SHA512_Final` with the SHA-512/224 or SHA-512/256 digest length. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw - [DES_set_key, DES_ecb_encrypt, DES_set_odd_parity](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the 128-byte `DES_key_schedule` holds the 8-byte key followed by zeros instead of LibreSSL's precomputed subkeys, so a title reading its contents sees different bytes; `DES_ecb_encrypt` derives the subkeys through mbedtls on each call. Not compared with the console. Results follow LibreSSL [des_key.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des_key.c) and [des.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des.c): `DES_set_key` skips the parity and weak-key checks and returns 0, as it does while `DES_check_key` is 0 (that variable is not in the PS5 export inventory), a nonzero `enc` encrypts, and `DES_set_odd_parity` sets each byte's low bit for odd parity. Null pointers throw where LibreSSL faults - [X509_verify_cert_error_string](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - no console call is verified. The 64-bit argument, low-32-bit lookup, messages and shared numeric fallback follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509/x509_txt.c), the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware versions remain unverified. Unknown-code results share an overwritten static buffer and are not safe for concurrent use, as in that release; code 1 and codes above 54 use the numeric fallback. @@ -10,6 +10,8 @@ - [AES_set_encrypt_key, AES_set_decrypt_key, AES_encrypt, AES_decrypt, AES_ecb_encrypt, AES_cbc_encrypt, AES_cfb128_encrypt, AES_ctr128_encrypt](../../../core/libs/prx/libSceLibreSsl/src/Aes.cpp) (libSceLibreSsl) - follow LibreSSL's [aes.h](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/aes/aes.h) and [mode code](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/modes). Nothing is checked on the console. Key setup follows [LibreSSL 2.5.4's aes_core.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/aes/aes_core.c), the version the `X509_verify_cert_error_string` entry names: a null pointer returns -1 and a key length other than 128, 192 or 256 bits returns -2 before the key is written, and only the 44, 52 or 60 words the key length needs are written. The `AES_KEY` schedule uses that file's generic C layout (big-endian words, `rounds` 10, 12 or 14). The 2.5.4 amd64 build uses [aes-x86_64](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/aes/asm/aes-x86_64.pl) instead, which copies the key bytes without swapping, so each word is byte-reversed compared with the C layout (OpenSSL 3.0.13's x86-64 code gives the same schedules byte-reversed); the console's build options are unknown, so a schedule the title reads or fills is unverified. CTR leaves the last keystream block in `ecount_buf` even at a block boundary. Null pointers in the cipher and mode functions, other `rounds` values and a CFB or CTR position outside 0-15 throw - [ERR_get_error, ERR_peek_error, ERR_clear_error, ERR_remove_thread_state](../../../core/libs/prx/libSceLibreSsl/src/Error.cpp) (libSceLibreSsl) - no export of this library adds an error yet, so the queue stays empty for guest code. The per-thread queue of 16 slots holding 15 codes, its order and the `ERR_PACK` layout follow LibreSSL's [err.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/err/err.c) and [err.h](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/err/err.h); the module's LibreSSL version is unknown and nothing is checked on the console. The queue keeps no file, line or data, as no export reads them. `ERR_remove_thread_state` with a thread id throws: that version asserts a null id, while older versions remove the state of the thread the id names +- [SSL_CIPHER_get_bits, SSL_CIPHER_get_name and SSL_CIPHER_get_version](../../../core/libs/prx/libSceLibreSsl/src/Cipher.cpp) - the 88-byte guest cipher layout and these calls are not console-verified. They follow [LibreSSL 2.5.4 ssl.h](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl.h) and [ssl_ciph.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl_ciph.c), matching the version in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware layouts remain unverified. Protocol labels use the full 64-bit cipher ID, not the protocol flags; TLS 1.2 ciphers still report `TLSv1/SSLv3`, as in that release. + ### Functional - [ASN1_INTEGER_get](../../../core/libs/prx/libSceLibreSsl/src/Asn1.cpp) has no console verification for nonpositive lengths. It follows [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/asn1/a_int.c#L363-L389): valid integer types return zero without reading data when length is zero or negative; invalid types return -1 first. Later LibreSSL versions reject negative lengths through `ASN1_INTEGER_get_int64`; that later contract is not used for this library. From c2273c8a7e782eb207169f327348fc4ab5cd22d3 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Sun, 25 Oct 2026 07:23:12 -0300 Subject: [PATCH 354/428] fix(check-compile): reject forwarded compiler actions --- docs/dev/TechnicalDebt.md | 2 +- tools/check_compile.py | 2 ++ tools/tests/test_check_compile_modes.py | 28 +++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index dde1757815..c41157ea7f 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -29,4 +29,4 @@ Throughout the project, every function at every stage either **does exactly what - `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows. - [Quick syntax checks](../../tools/check_compile.py) reject unexpanded `@response` arguments rather than parse compiler-specific quoting and risk retaining output flags. Expand response files in `compile_commands.json` before checking; the Ninja fallback expands them with `ninja -t compdb -x`. -- [Quick syntax checks](../../tools/check_compile.py) do not model every compiler option's operand count. A preprocessing mode following an unrecognized option without a joined value is rejected rather than risk removing that option's operand. Compiler wrappers with their own option grammar and forwarded preprocessing modes are not covered. +- [Quick syntax checks](../../tools/check_compile.py) do not model every compiler option's operand count. A preprocessing mode following an unrecognized option without a joined value is rejected rather than risk removing that option's operand. Compiler wrappers with their own option grammar are not covered. Forwarded frontend/preprocessor options (`-Xclang`, `-Xpreprocessor`, their joined forms and `-Wp,`) are rejected: their action and output operands are not normalized, so copying them could bypass syntax analysis or write outputs. See the [Clang option grammar](https://clang.llvm.org/docs/ClangCommandLineReference.html). diff --git a/tools/check_compile.py b/tools/check_compile.py index 8a0083c473..49103c620a 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -179,6 +179,8 @@ def syntax_arguments(arguments, directory, source): trimmed.append(argument) index += 1 continue + if not positional and (argument in ("-Xclang", "-Xpreprocessor") or argument.startswith(("-Xclang=", "-Xpreprocessor=", "-Wp,"))): + raise ValueError(f"unsupported forwarded compiler option {argument}") if not positional and argument in (*DROP_WITH_VALUE, *WITH_VALUE): if index + 1 == len(arguments): raise ValueError(f"missing operand for {argument}") diff --git a/tools/tests/test_check_compile_modes.py b/tools/tests/test_check_compile_modes.py index 1a7e3d867b..4158818e8d 100644 --- a/tools/tests/test_check_compile_modes.py +++ b/tools/tests/test_check_compile_modes.py @@ -152,6 +152,34 @@ class CheckCompileModeTests(unittest.TestCase): if expected: self.assertIn("positional_syntax_error", result.stderr) + def check_forwarded_options(self, variants): + compiler = self.compilers(("clang",))[0] + for flags in variants: + for field in ("arguments", "command"): + for text in ("int value;\n", "int forwarded_syntax_error = ;\n"): + with self.subTest(flags=flags, field=field, text=text), tempfile.TemporaryDirectory(prefix="syntax forwarded ") as name: + root = Path(name).resolve() + build = root / "build" + build.mkdir() + source = root / "unit.c" + source.write_text(text, encoding="utf-8") + object_file, dependency_file = build / "unit.o", build / "unit.d" + object_file.write_bytes(b"existing object output\n") + dependency_file.write_bytes(b"existing dependency output\n") + arguments = [compiler, *flags, "-c", str(source), "-o", str(object_file), "-MF", str(dependency_file)] + result = self.run_tool(root, build, source, arguments, field) + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("unsupported forwarded compiler option", result.stderr) + + def test_forwarded_clang_actions_are_rejected(self): + self.check_forwarded_options((("-Xclang", "-E"), ("-Xclang=-E",), ("-Xclang", "-emit-obj"))) + + def test_forwarded_preprocessor_actions_are_rejected(self): + self.check_forwarded_options((("-Xpreprocessor", "-E"), ("-Xpreprocessor", "-M"))) + + def test_comma_forwarded_preprocessor_actions_are_rejected(self): + self.check_forwarded_options((("-Wp,-E",), ("-Wp,-M,forwarded.d",))) + def test_include_directory_values_can_look_like_modes(self): def configure(root, build, source): directory = build / "-E" From 05eec3e725dd216e55a8da49318c24cd1e2c780e Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Sun, 25 Oct 2026 07:26:07 -0300 Subject: [PATCH 355/428] test(net): use canonical public declarations --- core/libs/tests/GuestNetProtocol.cpp | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/core/libs/tests/GuestNetProtocol.cpp b/core/libs/tests/GuestNetProtocol.cpp index 01f247b3e0..038c59f77d 100644 --- a/core/libs/tests/GuestNetProtocol.cpp +++ b/core/libs/tests/GuestNetProtocol.cpp @@ -5,21 +5,8 @@ #include #include #include -#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceNet/libSceNet.hpp" -extern "C" { -int APS5_VABI sceNetInit_nid_postfix(); -int APS5_VABI sceNetSocket(const char*, int, int, int); -int APS5_VABI sceNetSocketClose(int); -int APS5_VABI sceNetGetsockopt(int, int, int, void*, std::uint32_t*); -int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); -int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t); -int APS5_VABI sceNetGetsockname(int, void*, std::uint32_t*); -int APS5_VABI sceNetListen(int, int); -int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); -int APS5_VABI sceNetAccept(int, void*, std::uint32_t*); -int* APS5_VABI sceNetErrnoLoc(); -} static void Require(bool condition, std::source_location where = std::source_location::current()) { if (!condition) { From a28e9fc2347c7e54d56fd73ac2b1157707af7c74 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Sun, 25 Oct 2026 07:26:57 -0300 Subject: [PATCH 356/428] test(net): use canonical public declarations --- core/libs/tests/GuestNetKeepalive.cpp | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/core/libs/tests/GuestNetKeepalive.cpp b/core/libs/tests/GuestNetKeepalive.cpp index d8071ab65f..d8d8624a88 100644 --- a/core/libs/tests/GuestNetKeepalive.cpp +++ b/core/libs/tests/GuestNetKeepalive.cpp @@ -9,23 +9,10 @@ #include #include #include -#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceNet/libSceNet.hpp" +#include "prx/libkernel/libkernel.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" -extern "C" { -int APS5_VABI sceNetInit_nid_postfix(); -int APS5_VABI sceNetSocket(const char*, int, int, int); -int APS5_VABI sceNetSocketClose(int); -int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t); -int APS5_VABI sceNetGetsockname(int, void*, std::uint32_t*); -int APS5_VABI sceNetListen(int, int); -int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); -int APS5_VABI sceNetAccept(int, void*, std::uint32_t*); -int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); -int APS5_VABI sceNetGetsockopt(int, int, int, void*, std::uint32_t*); -int* APS5_VABI sceNetErrnoLoc(); -int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); -} static void Require(bool condition) { if (!condition) std::abort(); From 65a98e7f31b7b6051004e8e5519b10d69c2a3a6f Mon Sep 17 00:00:00 2001 From: javierUDP Date: Wed, 21 Oct 2026 11:34:31 -0300 Subject: [PATCH 357/428] test(relinker): execute the moved scalar compare The stub that moves a hazard scalar compare out of line is checked through its bytes but never run, so nothing confirms that the compare still produces the mask it would inline. The x86-64 execution section now calls the stub for both outcomes of a CMPSS with an unsafe predicate and checks the destination's lanes. --- .../codegen/tests/Amd64OnlyConverterTests.cpp | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index e18ac588f4..f752618c5f 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -2071,6 +2072,45 @@ void xsaveoptExecution() { require(std::memcmp(area + 160 + 3 * 16, pattern, 16) == 0 && std::memcmp(area + 576 + 3 * 16, pattern + 16, 16) == 0, "XSAVE did not store YMM3 in the standard format"); require(std::memcmp(restored, pattern, sizeof(pattern)) == 0, "XRSTOR did not restore YMM3 from the area XSAVE wrote"); } + +void scalarCompareExecution() { + const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); + const Bytes site = {0xF3, 0x0F, 0xC2, 0xC1, 0x05}; + const auto match = matcher->Match(site.data(), site.size()); + require(match && match->Lowering == Codegen::Amd64OnlyLowering::Trampoline, "scalar compare stub was not produced"); + auto body = match->StubBody; + body[match->ReturnBranchOffset] = 0xC3; + void* code = mmap(nullptr, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + require(code != MAP_FAILED, "cannot map executable memory for the stub"); + std::memcpy(code, body.data(), body.size()); + struct Case { + float left; + float right; + std::uint32_t mask; + const char* name; + }; + for (const auto& item : std::vector{{309.0f, 309.0f, 0xFFFFFFFFu, "309 is not less than 309"}, + {309.0f, 310.0f, 0u, "309 is less than 310"}}) { + alignas(16) float left[4] = {item.left, 1.0f, 2.0f, 3.0f}; + alignas(16) float right[4] = {item.right, 0.0f, 0.0f, 0.0f}; + alignas(16) std::uint32_t result[4] = {}; + asm volatile( + "movaps (%[left]), %%xmm0\n\t" + "movaps (%[right]), %%xmm1\n\t" + "sub $128, %%rsp\n\t" + "call *%[code]\n\t" + "add $128, %%rsp\n\t" + "movaps %%xmm0, (%[result])\n\t" + : + : [left] "r"(left), [right] "r"(right), [result] "r"(result), [code] "r"(code) + : "memory", "xmm0", "xmm1"); + require(result[0] == item.mask, item.name); + require(result[1] == std::bit_cast(1.0f) && result[2] == std::bit_cast(2.0f) + && result[3] == std::bit_cast(3.0f), + "scalar compare stub changed the upper lanes of its destination"); + } + munmap(code, 4096); +} #else void reciprocalExecution() {} void registerFormExecution() {} @@ -2117,6 +2157,7 @@ void rdtscpExecution() { #else void rdtscpExecution() {} void wideStoreExecution() {} +void scalarCompareExecution() {} #endif void scannerZeroTail() { @@ -2178,6 +2219,7 @@ int main() { converterRosettaLegacyScalarCompare(); rdtscpExecution(); wideStoreExecution(); + scalarCompareExecution(); wideStoreSplit(); converterWideStoreWindows(); converterClaimedFollower(); From 7ad69a9f1924b7bdd2ced910a9c50a652f0b7c26 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Sun, 25 Oct 2026 07:29:04 -0300 Subject: [PATCH 358/428] test(net): use canonical public declarations --- core/libs/tests/GuestNetNodelayReadback.cpp | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/core/libs/tests/GuestNetNodelayReadback.cpp b/core/libs/tests/GuestNetNodelayReadback.cpp index c427cca3cb..ad296cdba6 100644 --- a/core/libs/tests/GuestNetNodelayReadback.cpp +++ b/core/libs/tests/GuestNetNodelayReadback.cpp @@ -13,17 +13,9 @@ #include #include #include -#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceNet/libSceNet.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" -extern "C" { -int APS5_VABI sceNetInit_nid_postfix(); -int APS5_VABI sceNetSocket(const char*, int, int, int); -int APS5_VABI sceNetSocketClose(int); -int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); -int APS5_VABI sceNetGetsockopt(int, int, int, void*, std::uint32_t*); -int* APS5_VABI sceNetErrnoLoc(); -} static void Require(bool condition, std::source_location where = std::source_location::current()) { if (!condition) { From 454eb1f46a176a010998fc16a1db6f44e06522b0 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Sun, 25 Oct 2026 07:40:23 -0300 Subject: [PATCH 359/428] fix(check-compile): preserve safe forwarded operands --- docs/dev/TechnicalDebt.md | 2 +- tools/check_compile.py | 15 ++++++++++++++- tools/tests/test_check_compile_modes.py | 13 ++++++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index c41157ea7f..7c64266a4d 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -29,4 +29,4 @@ Throughout the project, every function at every stage either **does exactly what - `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows. - [Quick syntax checks](../../tools/check_compile.py) reject unexpanded `@response` arguments rather than parse compiler-specific quoting and risk retaining output flags. Expand response files in `compile_commands.json` before checking; the Ninja fallback expands them with `ninja -t compdb -x`. -- [Quick syntax checks](../../tools/check_compile.py) do not model every compiler option's operand count. A preprocessing mode following an unrecognized option without a joined value is rejected rather than risk removing that option's operand. Compiler wrappers with their own option grammar are not covered. Forwarded frontend/preprocessor options (`-Xclang`, `-Xpreprocessor`, their joined forms and `-Wp,`) are rejected: their action and output operands are not normalized, so copying them could bypass syntax analysis or write outputs. See the [Clang option grammar](https://clang.llvm.org/docs/ClangCommandLineReference.html). +- [Quick syntax checks](../../tools/check_compile.py) do not model every compiler option's operand count. A preprocessing mode following an unrecognized option without a joined value is rejected rather than risk removing that option's operand. Compiler wrappers with their own option grammar are not covered. Forwarded frontend actions (`-Xclang`, `-Xpreprocessor` and their joined forms) are rejected. Comma-forwarded options (`-Wp,`) allow only recognized macro/include operands; other options are rejected because unmodeled actions or output operands could bypass syntax analysis or write outputs. See the [Clang option grammar](https://clang.llvm.org/docs/ClangCommandLineReference.html). diff --git a/tools/check_compile.py b/tools/check_compile.py index 49103c620a..347b6efa95 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -159,6 +159,17 @@ def syntax_only(entry, source): return syntax_arguments(arguments, directory, source) +def validate_preprocessor_arguments(argument): + options = iter(argument[4:].split(",")) + for option in options: + if option in ("-D", "-U", "-I", "-F", "-include", "--include", "-imacros", "--imacros", + "-isystem", "-iquote", "-idirafter", "-isysroot"): + if not next(options, None): + raise ValueError(f"missing operand for {option} in {argument}") + elif not (option.startswith(("-D", "-U", "-I", "-F")) and len(option) > 2): + raise ValueError(f"unsupported forwarded compiler option {option} in {argument}") + + def syntax_arguments(arguments, directory, source): validate_arguments(arguments) for argument in arguments[1:]: @@ -179,7 +190,7 @@ def syntax_arguments(arguments, directory, source): trimmed.append(argument) index += 1 continue - if not positional and (argument in ("-Xclang", "-Xpreprocessor") or argument.startswith(("-Xclang=", "-Xpreprocessor=", "-Wp,"))): + if not positional and (argument in ("-Xclang", "-Xpreprocessor") or argument.startswith(("-Xclang=", "-Xpreprocessor="))): raise ValueError(f"unsupported forwarded compiler option {argument}") if not positional and argument in (*DROP_WITH_VALUE, *WITH_VALUE): if index + 1 == len(arguments): @@ -188,6 +199,8 @@ def syntax_arguments(arguments, directory, source): trimmed.extend(arguments[index:index + 2]) index += 2 continue + if not positional and argument.startswith("-Wp,"): + validate_preprocessor_arguments(argument) if not positional and (argument in (*DROP, *PREPROCESSING) or argument.startswith(("-MT", "-MF", "-MQ", "-MJ"))): index += 1 continue diff --git a/tools/tests/test_check_compile_modes.py b/tools/tests/test_check_compile_modes.py index 4158818e8d..7c412987b9 100644 --- a/tools/tests/test_check_compile_modes.py +++ b/tools/tests/test_check_compile_modes.py @@ -178,7 +178,7 @@ class CheckCompileModeTests(unittest.TestCase): self.check_forwarded_options((("-Xpreprocessor", "-E"), ("-Xpreprocessor", "-M"))) def test_comma_forwarded_preprocessor_actions_are_rejected(self): - self.check_forwarded_options((("-Wp,-E",), ("-Wp,-M,forwarded.d",))) + self.check_forwarded_options((("-Wp,-E",), ("-Wp,-E,-P",))) def test_include_directory_values_can_look_like_modes(self): def configure(root, build, source): @@ -188,6 +188,17 @@ class CheckCompileModeTests(unittest.TestCase): return ["-I", "-E"], '#include "only_here.h"\n#ifndef FROM_INCLUDE\n#error include lost\n#endif\n', "" self.check_modes(((),), configure=configure) + def test_forwarded_macro_options_are_preserved(self): + self.check_modes((("-Wp,-DOTHER=1,-UUNUSED",),)) + + def test_forwarded_include_operands_can_look_like_modes(self): + def configure(root, build, source): + directory = build / "-E" + directory.mkdir() + (directory / "forwarded.h").write_text("#define FROM_INCLUDE 1\n", encoding="utf-8") + return ["-Wp,-I,-E"], '#include "forwarded.h"\n#ifndef FROM_INCLUDE\n#error include lost\n#endif\n', "" + self.check_modes(((),), configure=configure) + def test_library_directory_values_can_look_like_modes(self): self.check_modes((("-L", "-E"),)) From 1c13cf5a99f03c55e0397ab0455ae5d941738d41 Mon Sep 17 00:00:00 2001 From: emirplus Date: Wed, 21 Oct 2026 22:44:45 +0300 Subject: [PATCH 360/428] fix(tools): decode sources with utf-8-sig and reconfigure streams in import_audit Specify utf-8-sig encoding when reading library source files in source_stubs so UTF-8 characters are preserved on Windows and leading BOMs are tolerated. Reconfigure stdout and stderr streams to UTF-8 with backslashreplace at the beginning of main, and specify UTF-8 encoding when writing test fixtures. --- tools/import_audit.py | 9 ++++---- tools/tests/test_import_audit.py | 35 +++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index 3e33d27568..03ab8499d3 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -603,7 +603,7 @@ def source_stub_classes(source): for library in sorted(item for item in Path(source).iterdir() if item.is_dir()): aliases = {} for file in nid_names.source_files(library): - for nid, name in nid_names.source_aliases(file.read_text(errors="ignore")): + for nid, name in nid_names.source_aliases(file.read_text(encoding="utf-8-sig", errors="ignore")): aliases.setdefault(name, set()).add(nid) group = progress.scan_library(library) if "shared_sources" in group: @@ -917,6 +917,10 @@ def render(summary, records, missing, stops, missing_hosts=(), located=None): def main(argv=None): + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace") + if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser(description="classify the imports of a converted game against the built system libraries") parser.add_argument("registries", nargs="+", type=Path, help="JSON written by 'relinker --registry'") parser.add_argument("--libs", action="append", required=True, help="directory of built .prx files; repeat for several") @@ -976,7 +980,4 @@ def main(argv=None): if __name__ == "__main__": - for stream in (sys.stdout, sys.stderr): - if hasattr(stream, "reconfigure"): - stream.reconfigure(encoding="utf-8", errors="replace") raise SystemExit(main()) diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 00fcfea96c..b95897e6ba 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -172,7 +172,7 @@ def build_pe_with_imports(dlls, names=()): def write_source(root, library, text): directory = Path(root) / library directory.mkdir(parents=True, exist_ok=True) - (directory / "Export.cpp").write_text(text) + (directory / "Export.cpp").write_text(text, encoding="utf-8") def write_registry(path, entries, encoding="utf-8"): @@ -1308,6 +1308,12 @@ class ImportAuditTests(unittest.TestCase): self.assertNotIn(("libSceTwin.native.prx", twin), stubs) self.assertNotEqual(nid_names.compute_nid("printf"), nid_names.compute_nid("printf_nid_postfix")) + def test_source_stubs_decodes_source_with_utf8_bom(self): + source = self.work.source + write_source(source, "libSceBom", "\ufeff" + STUB_SOURCE.format(name="sceBomPending")) + stubs = import_audit.source_stubs(source) + self.assertEqual(stubs.get(("libSceBom.prx", nid_names.compute_nid("sceBomPending"))), "sceBomPending") + def test_disambiguated_stubs_use_the_nid_of_their_symbol(self): source = self.work.source write_source(source, "libkernel", STUB_SOURCE.format(name="fstat_nid_disambig1_nid_postfix") @@ -2274,6 +2280,33 @@ class StrictProviderTests(unittest.TestCase): write_registry(registry, [("AAAAAAAAAAA", "libSceMissing.prx")]) self.assertEqual(work.run(registry, "--strict-providers")[0], 1) + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with patch.object(import_audit.sys, "stdout", out), patch.object(import_audit.sys, "stderr", err): + try: + import_audit.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + if __name__ == "__main__": unittest.main() From 225b6072fc75aa33534217afce278d64d982bc12 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Sun, 25 Oct 2026 07:47:42 -0300 Subject: [PATCH 361/428] fix(check-compile): reject forwarded response operands --- docs/dev/TechnicalDebt.md | 2 +- tools/check_compile.py | 5 ++++- tools/tests/test_check_compile_modes.py | 7 ++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 7c64266a4d..a9bed3b21f 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -29,4 +29,4 @@ Throughout the project, every function at every stage either **does exactly what - `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows. - [Quick syntax checks](../../tools/check_compile.py) reject unexpanded `@response` arguments rather than parse compiler-specific quoting and risk retaining output flags. Expand response files in `compile_commands.json` before checking; the Ninja fallback expands them with `ninja -t compdb -x`. -- [Quick syntax checks](../../tools/check_compile.py) do not model every compiler option's operand count. A preprocessing mode following an unrecognized option without a joined value is rejected rather than risk removing that option's operand. Compiler wrappers with their own option grammar are not covered. Forwarded frontend actions (`-Xclang`, `-Xpreprocessor` and their joined forms) are rejected. Comma-forwarded options (`-Wp,`) allow only recognized macro/include operands; other options are rejected because unmodeled actions or output operands could bypass syntax analysis or write outputs. See the [Clang option grammar](https://clang.llvm.org/docs/ClangCommandLineReference.html). +- [Quick syntax checks](../../tools/check_compile.py) do not model every compiler option's operand count. A preprocessing mode following an unrecognized option without a joined value is rejected rather than risk removing that option's operand. Compiler wrappers with their own option grammar are not covered. Forwarded frontend actions (`-Xclang`, `-Xpreprocessor` and their joined forms) are rejected. Comma-forwarded options (`-Wp,`) allow only recognized macro/include operands; response-file tokens are rejected even as operands, and other options are rejected because unmodeled actions or output operands could bypass syntax analysis or write outputs. See the [Clang option grammar](https://clang.llvm.org/docs/ClangCommandLineReference.html). diff --git a/tools/check_compile.py b/tools/check_compile.py index 347b6efa95..32934f8fb3 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -160,7 +160,10 @@ def syntax_only(entry, source): def validate_preprocessor_arguments(argument): - options = iter(argument[4:].split(",")) + forwarded = argument[4:].split(",") + if any(option.startswith("@") for option in forwarded): + raise ValueError(f"unsupported forwarded compiler option response file in {argument}") + options = iter(forwarded) for option in options: if option in ("-D", "-U", "-I", "-F", "-include", "--include", "-imacros", "--imacros", "-isystem", "-iquote", "-idirafter", "-isysroot"): diff --git a/tools/tests/test_check_compile_modes.py b/tools/tests/test_check_compile_modes.py index 7c412987b9..9664440b2c 100644 --- a/tools/tests/test_check_compile_modes.py +++ b/tools/tests/test_check_compile_modes.py @@ -152,7 +152,7 @@ class CheckCompileModeTests(unittest.TestCase): if expected: self.assertIn("positional_syntax_error", result.stderr) - def check_forwarded_options(self, variants): + def check_forwarded_options(self, variants, response_text=None): compiler = self.compilers(("clang",))[0] for flags in variants: for field in ("arguments", "command"): @@ -162,6 +162,8 @@ class CheckCompileModeTests(unittest.TestCase): build = root / "build" build.mkdir() source = root / "unit.c" + if response_text is not None: + (build / "operand.rsp").write_text(response_text, encoding="utf-8") source.write_text(text, encoding="utf-8") object_file, dependency_file = build / "unit.o", build / "unit.d" object_file.write_bytes(b"existing object output\n") @@ -180,6 +182,9 @@ class CheckCompileModeTests(unittest.TestCase): def test_comma_forwarded_preprocessor_actions_are_rejected(self): self.check_forwarded_options((("-Wp,-E",), ("-Wp,-E,-P",))) + def test_forwarded_response_operands_cannot_hide_actions(self): + self.check_forwarded_options((("-Wp,-I,@operand.rsp",),), response_text="somewhere -E") + def test_include_directory_values_can_look_like_modes(self): def configure(root, build, source): directory = build / "-E" From 7b557b5e079792c97f7d4b1d1c435f9a9dd6f550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 25 Oct 2026 11:48:44 +0100 Subject: [PATCH 362/428] test(tools): pin that the alias scanner receives BOM-free decoded text Co-Authored-By: Claude Sonnet 5.5 --- tools/tests/test_import_audit.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index b95897e6ba..7035c0615c 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -1314,6 +1314,21 @@ class ImportAuditTests(unittest.TestCase): stubs = import_audit.source_stubs(source) self.assertEqual(stubs.get(("libSceBom.prx", nid_names.compute_nid("sceBomPending"))), "sceBomPending") + def test_source_stubs_passes_decoded_text_without_bom_to_alias_scanner(self): + source = self.work.source + write_source(source, "libSceBomAlias", "\ufeff" + 'APS5_EXPORT("AAAAAAAAAAA", sceBomAlias);\n' + STUB_SOURCE.format(name="sceBomAlias")) + seen = [] + real = nid_names.source_aliases + + def spy(text): + seen.append(text) + return real(text) + + with patch.object(nid_names, "source_aliases", spy): + import_audit.source_stubs(source) + self.assertTrue(seen) + self.assertTrue(all(text.startswith("APS5_EXPORT(") for text in seen)) + def test_disambiguated_stubs_use_the_nid_of_their_symbol(self): source = self.work.source write_source(source, "libkernel", STUB_SOURCE.format(name="fstat_nid_disambig1_nid_postfix") From 11bc03f8f483047c02c1250f900382dfa035415e Mon Sep 17 00:00:00 2001 From: Eduardo Ruiz Date: Sun, 25 Oct 2026 11:51:10 +0100 Subject: [PATCH 363/428] fix(libc): keep the native Windows condition-variable path std::condition_variable_any regressed guest_libc_cnd_timed_wait on the Windows runner. Keep CRITICAL_SECTION/CONDITION_VARIABLE on Windows and poll TryEnterCriticalSection for _Mtx_timedlock. --- core/libs/prx/libc/src/Threads.cpp | 67 ++++++++++++++++++++++++++++++ docs/dev/debt/libc.md | 2 +- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index 8e9566b718..059b9afc45 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -75,8 +75,75 @@ constexpr int mutexRecursive = 0x100; constexpr std::uint64_t mutexTag = 0x5854'4D5F'4D54'5844ull; constexpr std::uint64_t conditionTag = 0x5854'4E43'5F44'4E43ull; +#ifdef _WIN32 +struct DinkumwareNativeMutex { + CRITICAL_SECTION value; + + DinkumwareNativeMutex() { + if (!InitializeCriticalSectionAndSpinCount(&value, 0)) + throw std::system_error(static_cast(GetLastError()), std::system_category(), "_Mtx_init: initializing critical section failed"); + } + + ~DinkumwareNativeMutex() { DeleteCriticalSection(&value); } + void lock() { EnterCriticalSection(&value); } + void unlock() { LeaveCriticalSection(&value); } + bool try_lock() { return TryEnterCriticalSection(&value) != FALSE; } + + template + bool try_lock_until(const std::chrono::time_point& deadline) { + if (try_lock()) + return true; + for (;;) { + const auto now = Clock::now(); + if (now >= deadline) + return false; + const auto remaining = std::chrono::ceil(deadline - now).count(); + Sleep(static_cast(std::min(remaining, 1))); + if (try_lock()) { + if (Clock::now() <= deadline) + return true; + unlock(); + return false; + } + } + } +}; + +struct DinkumwareNativeCondition { + CONDITION_VARIABLE value = CONDITION_VARIABLE_INIT; + + void notify_one() { WakeConditionVariable(&value); } + void notify_all() { WakeAllConditionVariable(&value); } + + void wait(std::unique_lock& lock) { + if (SleepConditionVariableCS(&value, &lock.mutex()->value, INFINITE) == FALSE) + throw std::system_error(static_cast(GetLastError()), std::system_category(), "_Cnd_wait: condition wait failed"); + } + + template + std::cv_status wait_until(std::unique_lock& lock, + const std::chrono::time_point& deadline) { + for (;;) { + const auto now = Clock::now(); + if (now >= deadline) + return std::cv_status::timeout; + const auto remaining = std::chrono::ceil(deadline - now).count(); + const auto largest = static_cast(std::numeric_limits::max() - 1U); + const auto milliseconds = static_cast(std::min(remaining, largest)); + if (SleepConditionVariableCS(&value, &lock.mutex()->value, milliseconds) != FALSE) + return std::cv_status::no_timeout; + const DWORD error = GetLastError(); + if (error != ERROR_TIMEOUT) + throw std::system_error(static_cast(error), std::system_category(), "_Cnd_timedwait: condition wait failed"); + if (Clock::now() >= deadline) + return std::cv_status::timeout; + } + } +}; +#else using DinkumwareNativeMutex = std::timed_mutex; using DinkumwareNativeCondition = std::condition_variable_any; +#endif struct DinkumwareMutex { std::uint64_t tag = mutexTag; diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 71db1cce83..9f4ee7b66c 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -37,7 +37,7 @@ - [__atomic_load, __atomic_store, __atomic_exchange and __atomic_compare_exchange](../../../core/libs/prx/libc/src/AtomicBuiltins.cpp) (libc) - the generic, size-parameterised libatomic entry points are a single `ud2` in the `libc.prx` bundled with PPSA08427 (disassembly), so the console traps on any call; these throw instead - [Sized __atomic calls](../../../core/libs/prx/libc/src/DinkumwareAtomics.cpp) (libc) - the 36 `__atomic_load_N`, `__atomic_store_N`, `__atomic_exchange_N`, `__atomic_compare_exchange_N` and `__atomic_fetch_{add,sub,and,or,xor}_N` calls (N = 1, 2, 4, 8) are sequentially consistent whatever order they are given. The `libc.prx` bundled with PPSA08427, run side by side on random values and on every order from -1 to 7, switches on the order but always uses a locked instruction or CAS loop, except that a store with an order other than `memory_order_seq_cst` is a plain store, so these are never weaker than the console. Unlike `_Atomic_load_8` and the 8-byte `_Atomic_*` exchange and fetch calls above, the console accepts every order here, including release, acq_rel and invalid ones, and these do the same - [Dinkumware mutexes and conditions](../../../core/libs/prx/libc/src/Threads.cpp) (libc) use pointer-to-handle arguments, matching the console aerolib signatures reported in [#3079](https://github.com/boykopovar/AnyPS5/pull/3079). The title's `_Mtx_lock` maps `0` to `_Thrd_success` (0), `0x8002000b` to `_Thrd_busy` (3), and other failures to `_Thrd_error` (4); its internal mutex init recognizes recursive flag `0x100`, while every other type uses default attributes. The observed relock failure for a plain mutex suggests those defaults are error-checking, but that has not been confirmed on hardware. The title's `_Mtx_unlock` and `_Cnd_wait` return success regardless of their internal result. The host implementation treats invalid ownership preconditions as no-ops, and throws for invalid handles, host wait errors and destroying a locked mutex as debug guards; those cases are not confirmed against the console. -- [_Mtx_timedlock](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `hPzYSd5Nasc`) follows the console's `libc.prx` at `0x5d50` (PPSA02530): it takes an absolute xtime and returns `_Thrd_timedout` (2) on expiry (`0x8002003c`) or 5 on other failures. The native mutex and condition are `std::timed_mutex` and `std::condition_variable_any`, replacing the Windows `CRITICAL_SECTION`/`CONDITION_VARIABLE` pair; Windows timing is not measured on the console. +- [_Mtx_timedlock](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `hPzYSd5Nasc`) follows the console's `libc.prx` at `0x5d50` (PPSA02530): it takes an absolute xtime and returns `_Thrd_timedout` (2) on expiry (`0x8002003c`) or 5 on other failures. On non-Windows hosts the native mutex and condition are `std::timed_mutex` and `std::condition_variable_any`; Windows keeps the `CRITICAL_SECTION`/`CONDITION_VARIABLE` pair because `std::condition_variable_any` failed `guest_libc_cnd_timed_wait` on the Windows runner, and its `_Mtx_timedlock` polls `TryEnterCriticalSection` until the deadline. Windows timing is not measured on the console. - [Floating-point compiler-rt builtins](../../../core/libs/prx/libc/src/FloatBuiltins.cpp) (libc) follow the `libc.prx` bundled with PPSA08427 (disassembly, and that binary run side by side on random inputs) where it differs from current compiler-rt: the soft-float `__add`, `__sub`, `__mul` and `__div` (`sf3`, `df3`) round to nearest, raise no exception flags and ignore MXCSR, here done with SSE under a default MXCSR that is restored afterwards; division flushes a quotient below the smallest normal to a signed zero; `__extendsfdf2` and `__truncdfsf2` copy NaN payloads unshifted; float-to-integer conversions do not saturate but shift the significand by a count masked to the shift width. `__mulsc3`, `__muldc3`, `__divsc3` and `__divdc3` fuse the same products as the console with `fma` and scale through the x87 like its `scalbn`, so their values match, but NaN payloads and exception flags can differ, as the host `fma` and `logb` are not the console's VEX FMA code and libm - [random, srandom, initstate, setstate](../../../core/libs/prx/libc/src/Random.cpp) (libc) - not measured: the state `setstate` returns and its null return for a rear position past the degree, `errno` after `initstate` below 8 bytes, a null state (throws here), an `initstate` seed other than 1, concurrent callers (one lock serialises every call here) and a title's own libc. Measured in a payload process on a PS5 (firmware 12.70): a fresh process draws the `srandom(1)` sequence; `srandom` seeds an additive feedback generator through a `good_rand` that reads its argument as an int32, turns 0 into 123459876 and applies `x = 16807 * (x % 127773) - 2836 * (x / 127773)`, adding 0x7fffffff to a negative result, which reproduces seeds 0, 1, 2, 12345, 0xffffffff and 0x80000000; `initstate` returns null at 0 and 7 bytes and selects types 0 to 4 by size as measured at 8, 15, 31, 32, 63, 64, 127, 128, 129, 255, 256, 257 and 1000 bytes. - [srandomdev](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) is not verified against the console. The firmware 3.20 [export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibcInternal.c) names NID `a2MOZf++Wjg`; it follows FreeBSD 11 [random.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdlib/random.c), filling every active generator word from host OS entropy instead of `KERN_ARND`, then resetting additive feedback positions without warming up. An entropy failure throws rather than leaving `sysctl`'s failure unchecked. Calls share the generator lock. From eb7519668f0f6cf1efd32a83340c92b7afe81db0 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Sat, 24 Oct 2026 10:31:02 +0300 Subject: [PATCH 364/428] feat(libSceSharePlay): report the disconnected state from sceSharePlayGetCurrentConnectionInfo --- core/libs/SceTypes.hpp | 9 +++++++++ core/libs/prx/libSceSharePlay/Export.cpp | 9 +++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index f51297c09b..0eb23cef6d 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1502,6 +1502,15 @@ struct SharePlayConnectionInfoA { std::int32_t visitorUserId; }; static_assert(sizeof(SharePlayConnectionInfoA) == 72 && offsetof(SharePlayConnectionInfoA, hostAccountId) == 48 && offsetof(SharePlayConnectionInfoA, hostUserId) == 64); +struct SharePlayConnectionInfo { + std::int32_t status; + std::int32_t mode; + NpOnlineId hostOnlineId; + NpOnlineId visitorOnlineId; + std::int32_t hostUserId; + std::int32_t visitorUserId; +}; +static_assert(sizeof(SharePlayConnectionInfo) == 56 && offsetof(SharePlayConnectionInfo, hostUserId) == 48); struct NpId { NpOnlineId online_id; std::uint8_t opaque[4]; }; struct NpCreateAsyncRequestParameter { std::uint8_t opaque[64]; }; struct NpCheckPremiumParameter { std::uint8_t opaque[64]; }; diff --git a/core/libs/prx/libSceSharePlay/Export.cpp b/core/libs/prx/libSceSharePlay/Export.cpp index 05910a7c56..a19d94eb98 100644 --- a/core/libs/prx/libSceSharePlay/Export.cpp +++ b/core/libs/prx/libSceSharePlay/Export.cpp @@ -51,8 +51,13 @@ int APS5_VABI sceSharePlayCrashDaemon(void) { return 0; } -int APS5_VABI sceSharePlayGetCurrentConnectionInfo(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceSharePlayGetCurrentConnectionInfo(SharePlayConnectionInfo* info) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED; + if (info == nullptr) return SCE_SHARE_PLAY_ERROR_INVALID_ARGS; + *info = {}; + info->hostUserId = SCE_USER_SERVICE_USER_ID_INVALID; + info->visitorUserId = SCE_USER_SERVICE_USER_ID_INVALID; return 0; } From 8e8ba8513bbea569f2f06e24a2a60a5eac1a75e6 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Sat, 24 Oct 2026 10:31:21 +0300 Subject: [PATCH 365/428] test(libSceSharePlay): cover the plain connection info query --- core/libs/prx/libSceSharePlay/include/Api.hpp | 2 +- core/libs/tests/GuestSharePlay.cpp | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceSharePlay/include/Api.hpp b/core/libs/prx/libSceSharePlay/include/Api.hpp index 6336eae897..317c0dc43c 100644 --- a/core/libs/prx/libSceSharePlay/include/Api.hpp +++ b/core/libs/prx/libSceSharePlay/include/Api.hpp @@ -9,7 +9,7 @@ int APS5_VABI sceSharePlayTerminate(void); int APS5_VABI sceSharePlayGetCurrentConnectionInfoA(SharePlayConnectionInfoA* info); int APS5_VABI sceSharePlayProhibitController(std::uint8_t prohibited); int APS5_VABI sceSharePlayCrashDaemon(void); -int APS5_VABI sceSharePlayGetCurrentConnectionInfo(void); +int APS5_VABI sceSharePlayGetCurrentConnectionInfo(SharePlayConnectionInfo* info); int APS5_VABI sceSharePlayGetCurrentInfo(void); int APS5_VABI sceSharePlayGetEvent(void); int APS5_VABI sceSharePlayNotifyDialogOpen(void); diff --git a/core/libs/tests/GuestSharePlay.cpp b/core/libs/tests/GuestSharePlay.cpp index 1bb330a5a8..c1545a1dad 100644 --- a/core/libs/tests/GuestSharePlay.cpp +++ b/core/libs/tests/GuestSharePlay.cpp @@ -1,4 +1,4 @@ -#include "prx/libSceSharePlay/include/Api.hpp" +#include "prx/libSceSharePlay/libSceSharePlay.hpp" #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include @@ -26,6 +26,7 @@ int main() { constexpr int invalidArgument = static_cast(0x810E0001); Require(sceSharePlayTerminate() == notInitialized); Require(sceSharePlayGetCurrentConnectionInfoA(nullptr) == notInitialized); + Require(sceSharePlayGetCurrentConnectionInfo(nullptr) == notInitialized); Require(sceSharePlayProhibitController(2) == notInitialized); Require(sceSharePlayInitialize(nullptr, sizeof(heap)) == 0); Require(sceSharePlayInitialize(heap, 0) == alreadyInitialized); @@ -41,6 +42,14 @@ int main() { Require(info.hostOnlineId.data[0] == 0 && info.visitorOnlineId.data[0] == 0 && info.hostOnlineId.pad[2] == 0); Require(info.hostUserId == -1 && info.visitorUserId == -1); Require(sceSharePlayGetCurrentConnectionInfoA(nullptr) == static_cast(0x810E0001)); + unsigned char plain[sizeof(SharePlayConnectionInfo) + 8]; + std::memset(plain, 0x5a, sizeof(plain)); + auto* plainInfo = reinterpret_cast(plain); + Require(sceSharePlayGetCurrentConnectionInfo(plainInfo) == 0); + Require(plainInfo->status == 0 && plainInfo->mode == 0 && plainInfo->hostOnlineId.data[0] == 0 && plainInfo->visitorOnlineId.pad[2] == 0); + Require(plainInfo->hostUserId == -1 && plainInfo->visitorUserId == -1); + for (std::size_t i = sizeof(SharePlayConnectionInfo); i < sizeof(plain); ++i) Require(plain[i] == 0x5a); + Require(sceSharePlayGetCurrentConnectionInfo(nullptr) == invalidArgument); Require(sceSharePlayProhibitController(1) == 0); Require(sceSharePlayProhibitController(1) == 0); Require(sceSharePlayProhibitController(0) == 0); From fd1cfeacedd71372d68e443f4fbfe0ec4ba97327 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Sat, 24 Oct 2026 10:31:30 +0300 Subject: [PATCH 366/428] docs(debt): note the plain SharePlay connection info layout --- docs/dev/debt/libSceSharePlay.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceSharePlay.md b/docs/dev/debt/libSceSharePlay.md index f7255df0d8..915f67c4c2 100644 --- a/docs/dev/debt/libSceSharePlay.md +++ b/docs/dev/debt/libSceSharePlay.md @@ -2,11 +2,12 @@ ### Unknown function info -- [sceSharePlayCrashDaemon, sceSharePlayGetCurrentConnectionInfo, sceSharePlayGetCurrentInfo, sceSharePlayGetEvent, sceSharePlayNotifyDialogOpen, sceSharePlayNotifyForceCloseForCdlg, sceSharePlayResumeScreenForCdlg, sceSharePlayServerLock](../../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - the list is the intersection of the export table of the console's [libSceSharePlay](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSharePlay.c) (function symbols only), the pinned [aerolib names](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) and the functions not yet defined anywhere in the repo. No credible prototypes were found; they are declared without parameters solely to resolve known imports, not as an ABI claim, and callers must not call them until signatures are verified. +- [sceSharePlayCrashDaemon, sceSharePlayGetCurrentInfo, sceSharePlayGetEvent, sceSharePlayNotifyDialogOpen, sceSharePlayNotifyForceCloseForCdlg, sceSharePlayResumeScreenForCdlg, sceSharePlayServerLock](../../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - the list is the intersection of the export table of the console's [libSceSharePlay](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSharePlay.c) (function symbols only), the pinned [aerolib names](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) and the functions not yet defined anywhere in the repo. No credible prototypes were found; they are declared without parameters solely to resolve known imports, not as an ABI claim, and callers must not call them until signatures are verified. - [sceSharePlayServerUnLock, sceSharePlaySetMode, sceSharePlaySetProhibition, sceSharePlaySetProhibitionModeWithAppId, sceSharePlayStartStandby, sceSharePlayStartStreaming, sceSharePlayStopStandby, sceSharePlayStopStreaming](../../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - the list is the intersection of the export table of the console's [libSceSharePlay](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSharePlay.c) (function symbols only), the pinned [aerolib names](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) and the functions not yet defined anywhere in the repo. No credible prototypes were found; they are declared without parameters solely to resolve known imports, not as an ABI claim, and callers must not call them until signatures are verified. ### Functional - [sceSharePlayInitialize and sceSharePlayTerminate](../../../core/libs/prx/libSceSharePlay/Export.cpp) - native heap validation and initialization state are recovered: a null heap ignores the supplied size, a supplied heap requires at least 0x1800 bytes, repeated initialization returns 0x810E0003 and termination before initialization returns 0x810E0004. The host does not allocate the native internal heap, connect to the Share Play service or reproduce its failures. - [sceSharePlayGetCurrentConnectionInfoA](../../../core/libs/prx/libSceSharePlay/Export.cpp) - the native module confirms a 72-byte result, initial zero fields and both user IDs -1. Initialization is checked before the output pointer. The host retains this initial disconnected state; native event draining, cached updates and service reconnection are unimplemented. +- [sceSharePlayGetCurrentConnectionInfo](../../../core/libs/prx/libSceSharePlay/Export.cpp) - the 56-byte layout without account IDs is from [shadPS4's shareplay.h](https://github.com/shadps4-emu/shadPS4/blob/396c2a09a38d22eaa02aab32c1967263dceab95f/src/core/libraries/share_play/shareplay.h); the checks and values copy `sceSharePlayGetCurrentConnectionInfoA`, while shadPS4 leaves both user IDs 0 and checks nothing. Not measured on a console. - [sceSharePlayProhibitController](../../../core/libs/prx/libSceSharePlay/Export.cpp) - the argument is one byte, forwarded unchanged to the service. The host checks initialization, accepts 0/1 without a visitor controller, and returns 0x810E0001 (SCE_SHARE_PLAY_ERROR_INVALID_ARGS) for other values; dynamic service communication and controller state remain unimplemented. From 7f295d046b1d3525bfe50160a7829aa31f003997 Mon Sep 17 00:00:00 2001 From: wrocketdev Date: Fri, 23 Oct 2026 01:32:44 +0200 Subject: [PATCH 367/428] feat(libSceLibreSsl): implement the OCSP status and reason strings Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceLibreSsl/CMakeLists.txt | 13 ++++- core/libs/prx/libSceLibreSsl/Export.cpp | 15 ------ core/libs/prx/libSceLibreSsl/src/Ocsp.cpp | 57 +++++++++++++++++++++ core/libs/tests/GuestLibreSslOcsp.cpp | 51 ++++++++++++++++++ docs/dev/debt/libSceLibreSsl.md | 3 +- 5 files changed, 122 insertions(+), 17 deletions(-) create mode 100644 core/libs/prx/libSceLibreSsl/src/Ocsp.cpp create mode 100644 core/libs/tests/GuestLibreSslOcsp.cpp diff --git a/core/libs/prx/libSceLibreSsl/CMakeLists.txt b/core/libs/prx/libSceLibreSsl/CMakeLists.txt index 9612c0369f..55c395d476 100644 --- a/core/libs/prx/libSceLibreSsl/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceLibreSsl SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Runtime.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Aes.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Error.cpp) +add_library(libSceLibreSsl SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Runtime.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Aes.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Error.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Ocsp.cpp) target_include_directories(libSceLibreSsl PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceLibreSsl PRIVATE mbedcrypto) configure_windows_unwind(libSceLibreSsl) @@ -77,4 +77,15 @@ if(BUILD_TESTING) set_tests_properties(guest_libressl_error PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_libressl_ocsp_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslOcsp.cpp) + target_include_directories(guest_libressl_ocsp_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_ocsp_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_ocsp_tests) + add_test(NAME guest_libressl_ocsp COMMAND guest_libressl_ocsp_tests) + set_tests_properties(guest_libressl_ocsp PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_ocsp PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 894b49bff5..e3fae68e26 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -813,21 +813,11 @@ int APS5_VABI OCSP_basic_verify_nid_postfix(void) { return 0; } -int APS5_VABI OCSP_cert_status_str_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI OCSP_check_validity_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI OCSP_crl_reason_str_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI OCSP_resp_count_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -848,11 +838,6 @@ int APS5_VABI OCSP_response_status_nid_postfix(void) { return 0; } -int APS5_VABI OCSP_response_status_str_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI OCSP_single_get0_status_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl/src/Ocsp.cpp b/core/libs/prx/libSceLibreSsl/src/Ocsp.cpp new file mode 100644 index 0000000000..bf12ef11cb --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/Ocsp.cpp @@ -0,0 +1,57 @@ +#include +#include +#include "prx/libc/include/general/VabiMacros.hpp" + +namespace { +struct OcspString { + std::int64_t code; + const char* text; +}; + +template +const char* TableToString(std::int64_t code, const OcspString (&table)[Count]) { + for (const auto& entry : table) { + if (entry.code == code) return entry.text; + } + return "(UNKNOWN)"; +} +} + +extern "C" { + +const char* APS5_VABI OCSP_response_status_str_nid_postfix(std::int64_t status) { + static constexpr OcspString table[] { + {0, "successful"}, + {1, "malformedrequest"}, + {2, "internalerror"}, + {3, "trylater"}, + {5, "sigrequired"}, + {6, "unauthorized"}, + }; + return TableToString(status, table); +} + +const char* APS5_VABI OCSP_cert_status_str_nid_postfix(std::int64_t status) { + static constexpr OcspString table[] { + {0, "good"}, + {1, "revoked"}, + {2, "unknown"}, + }; + return TableToString(status, table); +} + +const char* APS5_VABI OCSP_crl_reason_str_nid_postfix(std::int64_t reason) { + static constexpr OcspString table[] { + {0, "unspecified"}, + {1, "keyCompromise"}, + {2, "cACompromise"}, + {3, "affiliationChanged"}, + {4, "superseded"}, + {5, "cessationOfOperation"}, + {6, "certificateHold"}, + {8, "removeFromCRL"}, + }; + return TableToString(reason, table); +} + +} diff --git a/core/libs/tests/GuestLibreSslOcsp.cpp b/core/libs/tests/GuestLibreSslOcsp.cpp new file mode 100644 index 0000000000..3b2aadd543 --- /dev/null +++ b/core/libs/tests/GuestLibreSslOcsp.cpp @@ -0,0 +1,51 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +const char* APS5_VABI OCSP_response_status_str_nid_postfix(std::int64_t); +const char* APS5_VABI OCSP_cert_status_str_nid_postfix(std::int64_t); +const char* APS5_VABI OCSP_crl_reason_str_nid_postfix(std::int64_t); +} + +using StringFunction = const char* (APS5_VABI*)(std::int64_t); + +void Check(StringFunction function, const char* name, std::int64_t value, const char* expected) { + const char* actual = function(value); + if (actual == nullptr || std::strcmp(actual, expected) != 0) { + std::fprintf(stderr, "%s(%lld): expected %s, got %s\n", name, static_cast(value), expected, actual ? actual : "(null)"); + std::abort(); + } +} + +int main() { + constexpr auto Unknown = "(UNKNOWN)"; + constexpr std::int64_t Outside[] {-1, 4, 7, 9, 0x100, 0x100000000, std::numeric_limits::min(), std::numeric_limits::max()}; + + const char* responses[] {"successful", "malformedrequest", "internalerror", "trylater", nullptr, "sigrequired", "unauthorized"}; + for (std::int64_t code = 0; code < 7; ++code) { + Check(OCSP_response_status_str_nid_postfix, "OCSP_response_status_str", code, responses[code] ? responses[code] : Unknown); + } + + const char* certificates[] {"good", "revoked", "unknown"}; + for (std::int64_t code = 0; code < 3; ++code) { + Check(OCSP_cert_status_str_nid_postfix, "OCSP_cert_status_str", code, certificates[code]); + } + + const char* reasons[] {"unspecified", "keyCompromise", "cACompromise", "affiliationChanged", "superseded", "cessationOfOperation", "certificateHold", nullptr, "removeFromCRL"}; + for (std::int64_t code = 0; code < 9; ++code) { + Check(OCSP_crl_reason_str_nid_postfix, "OCSP_crl_reason_str", code, reasons[code] ? reasons[code] : Unknown); + } + + for (const auto value : Outside) { + Check(OCSP_response_status_str_nid_postfix, "OCSP_response_status_str", value, Unknown); + Check(OCSP_cert_status_str_nid_postfix, "OCSP_cert_status_str", value, Unknown); + if (value != 4) Check(OCSP_crl_reason_str_nid_postfix, "OCSP_crl_reason_str", value, Unknown); + } + Check(OCSP_response_status_str_nid_postfix, "OCSP_response_status_str", 0x100000000 | 1, Unknown); + Check(OCSP_crl_reason_str_nid_postfix, "OCSP_crl_reason_str", -0x100000000 | 8, Unknown); + return 0; +} diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index bbc0e9fb0e..d578cb70b6 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -2,13 +2,14 @@ ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 348 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 345 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. - [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); the module's LibreSSL version is unknown and nothing is checked on the console. Behaviour that differs between LibreSSL versions throws: `SHA256` with a null digest buffer (older versions return a static buffer) and `SHA512_Final` with the SHA-512/224 or SHA-512/256 digest length. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw - [DES_set_key, DES_ecb_encrypt, DES_set_odd_parity](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the 128-byte `DES_key_schedule` holds the 8-byte key followed by zeros instead of LibreSSL's precomputed subkeys, so a title reading its contents sees different bytes; `DES_ecb_encrypt` derives the subkeys through mbedtls on each call. Not compared with the console. Results follow LibreSSL [des_key.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des_key.c) and [des.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des.c): `DES_set_key` skips the parity and weak-key checks and returns 0, as it does while `DES_check_key` is 0 (that variable is not in the PS5 export inventory), a nonzero `enc` encrypts, and `DES_set_odd_parity` sets each byte's low bit for odd parity. Null pointers throw where LibreSSL faults - [X509_verify_cert_error_string](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - no console call is verified. The 64-bit argument, low-32-bit lookup, messages and shared numeric fallback follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509/x509_txt.c), the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware versions remain unverified. Unknown-code results share an overwritten static buffer and are not safe for concurrent use, as in that release; code 1 and codes above 54 use the numeric fallback. - [RAND_add, RAND_bytes, RAND_status, RAND_load_file, RAND_file_name, CRYPTO_malloc, CRYPTO_free, CRYPTO_set_mem_functions](../../../core/libs/prx/libSceLibreSsl/src/Runtime.cpp) (libSceLibreSsl) - follow LibreSSL's [rand_lib.c, randfile.c](https://github.com/libressl/openbsd/tree/0f0b16dc977f6ca499f0f4540078d992c96da597/src/lib/libcrypto/rand) and [malloc-wrapper.c](https://github.com/libressl/openbsd/blob/0f0b16dc977f6ca499f0f4540078d992c96da597/src/lib/libcrypto/malloc-wrapper.c), which are the same in 2.6.5 (3.4.3 linked); nothing is checked on the console. `RAND_bytes` reads host entropy and `CRYPTO_malloc` and `CRYPTO_free` use the guest heap. `CRYPTO_malloc` returns null for a size of zero or less, as LibreSSL 2.5.4 to 3.4 do; later versions take a `size_t` size and call `malloc`. A null buffer with a positive length throws - [AES_set_encrypt_key, AES_set_decrypt_key, AES_encrypt, AES_decrypt, AES_ecb_encrypt, AES_cbc_encrypt, AES_cfb128_encrypt, AES_ctr128_encrypt](../../../core/libs/prx/libSceLibreSsl/src/Aes.cpp) (libSceLibreSsl) - follow LibreSSL's [aes.h](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/aes/aes.h) and [mode code](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/modes). Nothing is checked on the console. Key setup follows [LibreSSL 2.5.4's aes_core.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/aes/aes_core.c), the version the `X509_verify_cert_error_string` entry names: a null pointer returns -1 and a key length other than 128, 192 or 256 bits returns -2 before the key is written, and only the 44, 52 or 60 words the key length needs are written. The `AES_KEY` schedule uses that file's generic C layout (big-endian words, `rounds` 10, 12 or 14). The 2.5.4 amd64 build uses [aes-x86_64](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/aes/asm/aes-x86_64.pl) instead, which copies the key bytes without swapping, so each word is byte-reversed compared with the C layout (OpenSSL 3.0.13's x86-64 code gives the same schedules byte-reversed); the console's build options are unknown, so a schedule the title reads or fills is unverified. CTR leaves the last keystream block in `ecount_buf` even at a block boundary. Null pointers in the cipher and mode functions, other `rounds` values and a CFB or CTR position outside 0-15 throw - [ERR_get_error, ERR_peek_error, ERR_clear_error, ERR_remove_thread_state](../../../core/libs/prx/libSceLibreSsl/src/Error.cpp) (libSceLibreSsl) - no export of this library adds an error yet, so the queue stays empty for guest code. The per-thread queue of 16 slots holding 15 codes, its order and the `ERR_PACK` layout follow LibreSSL's [err.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/err/err.c) and [err.h](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/err/err.h); the module's LibreSSL version is unknown and nothing is checked on the console. The queue keeps no file, line or data, as no export reads them. `ERR_remove_thread_state` with a thread id throws: that version asserts a null id, while older versions remove the state of the thread the id names +- [OCSP_response_status_str, OCSP_cert_status_str and OCSP_crl_reason_str](../../../core/libs/prx/libSceLibreSsl/src/Ocsp.cpp) (libSceLibreSsl) - the module's strings are not measured on the console. The tables follow LibreSSL [ocsp_prn.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/ocsp/ocsp_prn.c), unchanged from revision 1.8 (2015) to 1.12 (2025), with codes from `ocsp.h`; the argument is a 64-bit `long` and every value not in a table returns `(UNKNOWN)` ### Functional From 41eb9b983c729d995743335bf002f78e9f0f8c0d Mon Sep 17 00:00:00 2001 From: wrocketdev Date: Sun, 25 Oct 2026 02:12:19 +0100 Subject: [PATCH 368/428] refactor(libSceLibreSsl): declare the OCSP string functions in the public header Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceLibreSsl/include/Api.hpp | 6 +++--- core/libs/prx/libSceLibreSsl/src/Ocsp.cpp | 2 +- core/libs/tests/GuestLibreSslOcsp.cpp | 7 +------ 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceLibreSsl/include/Api.hpp b/core/libs/prx/libSceLibreSsl/include/Api.hpp index b23740b20a..cc54ae5595 100644 --- a/core/libs/prx/libSceLibreSsl/include/Api.hpp +++ b/core/libs/prx/libSceLibreSsl/include/Api.hpp @@ -219,14 +219,14 @@ int APS5_VABI OCSP_BASICRESP_free_nid_postfix(void); int APS5_VABI OCSP_RESPONSE_free_nid_postfix(void); int APS5_VABI OCSP_basic_add1_cert_nid_postfix(void); int APS5_VABI OCSP_basic_verify_nid_postfix(void); -int APS5_VABI OCSP_cert_status_str_nid_postfix(void); +const char* APS5_VABI OCSP_cert_status_str_nid_postfix(std::int64_t status); int APS5_VABI OCSP_check_validity_nid_postfix(void); -int APS5_VABI OCSP_crl_reason_str_nid_postfix(void); +const char* APS5_VABI OCSP_crl_reason_str_nid_postfix(std::int64_t reason); int APS5_VABI OCSP_resp_count_nid_postfix(void); int APS5_VABI OCSP_resp_get0_nid_postfix(void); int APS5_VABI OCSP_response_get1_basic_nid_postfix(void); int APS5_VABI OCSP_response_status_nid_postfix(void); -int APS5_VABI OCSP_response_status_str_nid_postfix(void); +const char* APS5_VABI OCSP_response_status_str_nid_postfix(std::int64_t status); int APS5_VABI OCSP_single_get0_status_nid_postfix(void); int APS5_VABI OPENSSL_add_all_algorithms_noconf_nid_postfix(void); int APS5_VABI OPENSSL_load_builtin_modules_nid_postfix(void); diff --git a/core/libs/prx/libSceLibreSsl/src/Ocsp.cpp b/core/libs/prx/libSceLibreSsl/src/Ocsp.cpp index bf12ef11cb..e64c504978 100644 --- a/core/libs/prx/libSceLibreSsl/src/Ocsp.cpp +++ b/core/libs/prx/libSceLibreSsl/src/Ocsp.cpp @@ -1,6 +1,6 @@ +#include "prx/libSceLibreSsl/include/Api.hpp" #include #include -#include "prx/libc/include/general/VabiMacros.hpp" namespace { struct OcspString { diff --git a/core/libs/tests/GuestLibreSslOcsp.cpp b/core/libs/tests/GuestLibreSslOcsp.cpp index 3b2aadd543..f0c3f82ad8 100644 --- a/core/libs/tests/GuestLibreSslOcsp.cpp +++ b/core/libs/tests/GuestLibreSslOcsp.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceLibreSsl/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -5,12 +6,6 @@ #include #include -extern "C" { -const char* APS5_VABI OCSP_response_status_str_nid_postfix(std::int64_t); -const char* APS5_VABI OCSP_cert_status_str_nid_postfix(std::int64_t); -const char* APS5_VABI OCSP_crl_reason_str_nid_postfix(std::int64_t); -} - using StringFunction = const char* (APS5_VABI*)(std::int64_t); void Check(StringFunction function, const char* name, std::int64_t value, const char* expected) { From c05345f529b21d9e53fa29efa672be11c2f2af6c Mon Sep 17 00:00:00 2001 From: JJenkx Date: Sat, 24 Oct 2026 11:32:05 -0400 Subject: [PATCH 369/428] fix(libc): preserve acoshf log and reciprocal evaluation --- core/libs/prx/libc/src/Math.cpp | 4 ++-- docs/dev/debt/libc.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index e635b0b1e1..4e53efaec8 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -1045,10 +1045,10 @@ float APS5_VABI acoshf_nid_postfix(float x) { if ((bits & 0x80000000u) != 0 || magnitude < 0x3f800000u) return (x - x) / (x - x); if (magnitude >= 0x7f800000u) return x + x; if (magnitude == 0x3f800000u) return 0.0f; - if (magnitude >= 0x4d800000u) return std::log(x) + ln2; + if (magnitude >= 0x4d800000u) return logf_nid_postfix(x) + ln2; if (magnitude > 0x40000000u) { const float square = x * x; - return std::log(2.0f * x - 1.0f / (x + std::sqrt(square - 1.0f))); + return logf_nid_postfix(2.0f * x + (-1.0f / (x + std::sqrt(square - 1.0f)))); } const float t = x - 1.0f; return AtanhfLog1p(t + std::sqrt(2.0f * t + t * t)); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 5171721bc8..586cbb6caa 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -111,7 +111,7 @@ - [expm1](../../../core/libs/prx/libc/src/Math.cpp) (libc) does not compare floating-point exceptions, and its double inputs are checked by sampling, not exhaustively. It is FreeBSD msun's [`s_expm1.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_expm1.c) with `huge` read at run time, and sets `ERANGE` only when a finite argument overflows to infinity, as the titles' `libc.prx` does. It gives the same bits and `errno` as the `expm1` bundled with PPSA07923 and PPSA05916 on 320 million random and 47,222 structured arguments on a Windows host; the FMA-contracted build bundled with PPSA13934 differs on 43,433 of the random ones. - [log1p](../../../core/libs/prx/libc/src/Math.cpp) (libc) does not compare floating-point exceptions, and its double inputs are checked by sampling, not exhaustively. It is FreeBSD msun's [`s_log1p.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_log1p.c). As the titles' `libc.prx` does, it sets `EDOM` at msun's NaN return (arguments below -1, `-inf` and negative NaNs) and leaves `errno` alone for `log1p(-1)`, where it returns `-inf`. It gives the same bits, NaN payloads included, and `errno` as the `log1p` bundled with PPSA07923 and PPSA05916 on 320 million random and 68,216 structured arguments on a Windows host. The FMA-contracted build bundled with PPSA13934 differs on 73,682 of the random ones. - [sinhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `sinhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_sinhf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_sinhf.c) from before `__ldexp_expf`, which squares `expf(|x| / 2)` between `logf(FLT_MAX)` and the overflow threshold, calling msun's [`expm1f`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_expm1f.c) and [`expf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_expf.c), whose paths for the arguments it passes are inlined here. Past the threshold Sony's error helper sets `errno` to `ERANGE` before returning `x * 1e37`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits as this implementation for every finite argument up to the threshold, of both signs; it was not run on Linux. That the system `libSceLibcInternal`, which Ruffle (PPSA68091) and RetroArch (PPSA99169) import while bundling a zero-return `libc.prx`, has the same code is assumed -- [acoshf](../../../core/libs/prx/libc/src/Math.cpp) (libc) uses FreeBSD msun's [`e_acoshf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_acoshf.c) range handling; the Windows UCRT differed from the `libc.prx` copies in PPSA07923 and PPSA13934 on 216,017 and 216,024 of 20 million inputs in the #9707 survey. The invalid-domain path raises `FE_INVALID` without setting `errno` as the source does. The exact system `libSceLibcInternal` behavior and FMA-contracted results remain unverified +- [acoshf](../../../core/libs/prx/libc/src/Math.cpp) (libc) does not reproduce bundled invalid-domain errno or x87 exception flags (#10610); exact system `libSceLibcInternal` behavior and FMA-contracted copies remain unverified. Positive finite inputs use [msun](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_acoshf.c) range handling and this file's `logf`; the middle range divides -1 before addition under directed rounding. Bits, errno and SSE/x87 status and controls match the supplied PPSA03766/PPSA07923 copies on 236,592 positive finite calls (4,929 inputs, four rounding modes, DAZ/FTZ combinations and sticky flags); these samples are not exhaustive. - [acoshl, asinhl, atanhl, atan2l, cbrtl, erfl, erfcl, expm1l, hypotl, log1pl, log2l, lgammal, tgammal, j0f, j1f, jnf, y0f, y1f and ynf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return the host C library's results (the float Bessel functions through its double ones), not those of the console's own code, so the last bits can differ (`y0f`, `y1f`, `ynf` follow the console at `+-0`). As in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x56410), `lgammal` does not store the sign in `signgam`; `hypotl` returns infinity for an infinite argument even when the other is a NaN, as C11 F.10.4.3 requires. [hypot3, hypot3f and hypot3l](../../../core/libs/prx/libc/src/Math.cpp) follow that module (0x51D30, 0x520D0, 0x52420): the first NaN of x, y and z is returned unchanged, any infinity gives positive infinity, a zero argument gives the two-argument hypot of the other two, and otherwise the largest magnitude is combined with the hypot of the other two - [asinl](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `asinl` of the `libc.prx` bundled with PPSA28319 (disassembly), FreeBSD msun's [`e_asinl.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/src/e_asinl.c) with the ld80 [`invtrig.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/ld80/invtrig.c) coefficients, which the binary holds as the same 80-bit constants. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, so a NaN argument also sets `EDOM`; msun itself sets no `errno`. 20 million random arguments in (-1, 1) give the same bits as that code run on a Windows host. The result needs the x87 precision control at 64 bits, as FreeBSD amd64 starts threads (control word `0x37f`), but guest threads do not set it: on Windows a thread started by `_beginthreadex` runs at 53 bits (`0x27f`), where `asinl(0.75)` returns mantissa `0xd91a98ae3406e000` instead of `0xd91a98ae3406e041`. The system `libSceLibcInternal` that RetroArch (PPSA99169) imports is assumed to have the same code - [atanhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `atanhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_atanhf.c`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_atanhf.c) calling msun's [`log1pf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_log1pf.c), whose paths for the positive arguments it passes are inlined here; the exported `log1pf` is still the host's. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, and for |x| = 1 it raises `FE_DIVBYZERO` and sets `errno` to `ERANGE` before returning `x / 0`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits, `errno` and floating-point exceptions as this implementation for all 2^32 arguments; it was not run on Linux. That the system `libSceLibcInternal`, which Dusklight (PPSA99305) imports while bundling a zero-return `libc.prx`, has the same code is assumed From 0e895b55be29357f6c4bbcd7a10c37965816941e Mon Sep 17 00:00:00 2001 From: JJenkx Date: Sat, 24 Oct 2026 11:29:18 -0400 Subject: [PATCH 370/428] test(libc): cover acoshf positive results across rounding modes --- core/libs/tests/GuestMath.cpp | 46 +++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 7c6bd1e44e..4b2d8bcb55 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -937,6 +937,52 @@ static void CheckAcoshfMsunBoundaries() { Require(std::isinf(acoshf_nid_postfix(INFINITY)) && acoshf_nid_postfix(INFINITY) > 0.0f); Require(std::isnan(acoshf_nid_postfix(-INFINITY))); Require(std::isnan(acoshf_nid_postfix(std::numeric_limits::quiet_NaN()))); + const RestoreFloatingPointState saved; + const unsigned savedCsr = _mm_getcsr(); + const struct { + std::uint32_t input; + std::uint32_t result[4]; + unsigned raised; + } cases[] = { + {0x3f800000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, 0u}, + {0x3f800001u, {0x39ffffffu, 0x39ffffffu, 0x3a000001u, 0x39ffffffu}, 32u}, + {0x3fffffffu, {0x3fa89213u, 0x3fa89212u, 0x3fa89214u, 0x3fa89212u}, 32u}, + {0x40000000u, {0x3fa89214u, 0x3fa89213u, 0x3fa89214u, 0x3fa89213u}, 32u}, + {0x40000001u, {0x3fa89215u, 0x3fa89214u, 0x3fa89215u, 0x3fa89214u}, 32u}, + {0x40000002u, {0x3fa89216u, 0x3fa89215u, 0x3fa89217u, 0x3fa89215u}, 32u}, + {0x40000003u, {0x3fa89217u, 0x3fa89217u, 0x3fa89218u, 0x3fa89217u}, 32u}, + {0x4d7fffffu, {0x41a0cf66u, 0x41a0cf65u, 0x41a0cf66u, 0x41a0cf65u}, 32u}, + {0x4d800000u, {0x41a0cf66u, 0x41a0cf64u, 0x41a0cf66u, 0x41a0cf64u}, 32u}, + {0x4d800001u, {0x41a0cf66u, 0x41a0cf65u, 0x41a0cf67u, 0x41a0cf65u}, 32u}, + {0x7f7fffffu, {0x42b2d4fcu, 0x42b2d4fbu, 0x42b2d4fdu, 0x42b2d4fbu}, 32u}, + }; + const int rounding[] = {FE_TONEAREST, FE_DOWNWARD, FE_UPWARD, FE_TOWARDZERO}; + float (APS5_VABI * volatile call)(float) = acoshf_nid_postfix; + for (unsigned mode = 0; mode != 4; ++mode) { + for (unsigned denorm = 0; denorm != 4; ++denorm) { + const unsigned control = 0x1f80u | (mode << 13u) | ((denorm & 1u) ? 0x40u : 0u) | ((denorm & 2u) ? 0x8000u : 0u); + for (std::uint16_t sticky : {0u, 5u, 32u}) { + for (const auto& item : cases) { + Require(std::fesetenv(FE_DFL_ENV) == 0 && std::fesetround(rounding[mode]) == 0); + Require(fesetexceptflag_nid_postfix(&sticky, 0x3f) == 0); + _mm_setcsr(control | sticky); + GuestFloatingPointEnvironment before; + Require(fegetenv_nid_postfix(&before) == 0 && (before.status & 0x3fu) == sticky); + errno = 1234; + const auto result = std::bit_cast(call(std::bit_cast(item.input))); + const int error = errno; + GuestFloatingPointEnvironment after; + Require(fegetenv_nid_postfix(&after) == 0); + if (result != item.result[mode] || after.mxcsr != (control | sticky | item.raised) || (after.status & 0x3fu) != sticky || after.control != before.control || error != 1234) { + std::fprintf(stderr, "acoshf positive: input=%08llx mode=%u denorm=%u sticky=%u bits=%08llx expected=%08llx csr=%x x87=%x errno=%d\n", static_cast(item.input), mode, denorm, sticky, static_cast(result), static_cast(item.result[mode]), after.mxcsr, after.status, error); + Require(false); + } + } + } + } + } + _mm_setcsr(savedCsr); + } static void CheckCoshfMsunBoundaries() { From 2fc4ca2fd0f951e29fbb2cbdfb538ccb81e907a1 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Sun, 25 Oct 2026 07:53:43 -0400 Subject: [PATCH 371/428] test(libc): reject host log fallback in large acoshf inputs --- core/libs/tests/GuestMath.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 4b2d8bcb55..af198e192b 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -954,6 +954,7 @@ static void CheckAcoshfMsunBoundaries() { {0x4d7fffffu, {0x41a0cf66u, 0x41a0cf65u, 0x41a0cf66u, 0x41a0cf65u}, 32u}, {0x4d800000u, {0x41a0cf66u, 0x41a0cf64u, 0x41a0cf66u, 0x41a0cf64u}, 32u}, {0x4d800001u, {0x41a0cf66u, 0x41a0cf65u, 0x41a0cf67u, 0x41a0cf65u}, 32u}, + {0x4d8050f2u, {0x41a0d473u, 0x41a0d472u, 0x41a0d474u, 0x41a0d472u}, 32u}, {0x7f7fffffu, {0x42b2d4fcu, 0x42b2d4fbu, 0x42b2d4fdu, 0x42b2d4fbu}, 32u}, }; const int rounding[] = {FE_TONEAREST, FE_DOWNWARD, FE_UPWARD, FE_TOWARDZERO}; From 1cf017b1891765fb8dcac7b30f75fc08fa42425e Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sun, 25 Oct 2026 15:03:02 +0300 Subject: [PATCH 372/428] feat(nid): add executable import patching with optional sce filter --- core/libs/nid/include/nid/ElfPatcher.hpp | 13 +- core/libs/nid/include/nid/IBinaryPatcher.hpp | 7 +- core/libs/nid/include/nid/MachONidPatcher.hpp | 2 +- core/libs/nid/include/nid/NidResolver.hpp | 5 + core/libs/nid/include/nid/PeNidPatcher.hpp | 2 +- core/libs/nid/src/ElfNidPatcher.cpp | 95 ++++- core/libs/nid/src/MachONidPatcher.cpp | 157 ++++--- core/libs/nid/src/NidResolver.cpp | 27 +- core/libs/nid/src/PeNidPatcher.cpp | 389 +++++++++++------- core/libs/nid/src/main.cpp | 13 +- core/libs/nid/tests/test_macho_nids.py | 2 +- core/libs/nid/tests/test_pe_references.py | 173 ++++++++ docs/dev/debt/libs.md | 2 + docs/user/USAGE.md | 10 + 14 files changed, 651 insertions(+), 246 deletions(-) diff --git a/core/libs/nid/include/nid/ElfPatcher.hpp b/core/libs/nid/include/nid/ElfPatcher.hpp index 0415b4eb7f..f6887aae64 100644 --- a/core/libs/nid/include/nid/ElfPatcher.hpp +++ b/core/libs/nid/include/nid/ElfPatcher.hpp @@ -22,6 +22,17 @@ struct Elf64_Ehdr { std::uint16_t e_shstrndx; }; +struct Elf64_Phdr { + std::uint32_t p_type; + std::uint32_t p_flags; + std::uint64_t p_offset; + std::uint64_t p_vaddr; + std::uint64_t p_paddr; + std::uint64_t p_filesz; + std::uint64_t p_memsz; + std::uint64_t p_align; +}; + struct Elf64_Shdr { std::uint32_t sh_name; std::uint32_t sh_type; @@ -103,7 +114,7 @@ constexpr std::uint16_t kShnUndef = 0u; class ElfNidPatcher final : public IBinaryPatcher { public: - void PatchNids(std::vector& binary, const std::string& libraryName, const std::unordered_set& excludedExports) const override; + void PatchNids(std::vector& binary, const std::string& libraryName, const std::unordered_set& excludedExports, const PatchOptions& options = {}) const override; }; } diff --git a/core/libs/nid/include/nid/IBinaryPatcher.hpp b/core/libs/nid/include/nid/IBinaryPatcher.hpp index 0fbcb88501..4e8cff5cbc 100644 --- a/core/libs/nid/include/nid/IBinaryPatcher.hpp +++ b/core/libs/nid/include/nid/IBinaryPatcher.hpp @@ -8,11 +8,16 @@ namespace Nid { +struct PatchOptions { + bool executable = false; + bool onlySce = false; +}; + class IBinaryPatcher { public: virtual ~IBinaryPatcher() = default; - virtual void PatchNids(std::vector& binary, const std::string& libraryName, const std::unordered_set& excludedExports) const = 0; + virtual void PatchNids(std::vector& binary, const std::string& libraryName, const std::unordered_set& excludedExports, const PatchOptions& options = {}) const = 0; }; } diff --git a/core/libs/nid/include/nid/MachONidPatcher.hpp b/core/libs/nid/include/nid/MachONidPatcher.hpp index f49732cd88..f6ae370549 100644 --- a/core/libs/nid/include/nid/MachONidPatcher.hpp +++ b/core/libs/nid/include/nid/MachONidPatcher.hpp @@ -22,7 +22,7 @@ std::vector ReadMachOExports(const std::vector& binar class MachONidPatcher final : public IBinaryPatcher { public: - void PatchNids(std::vector& binary, const std::string& libraryName, const std::unordered_set& excludedExports) const override; + void PatchNids(std::vector& binary, const std::string& libraryName, const std::unordered_set& excludedExports, const PatchOptions& options = {}) const override; }; } diff --git a/core/libs/nid/include/nid/NidResolver.hpp b/core/libs/nid/include/nid/NidResolver.hpp index df144b4519..9822939e4e 100644 --- a/core/libs/nid/include/nid/NidResolver.hpp +++ b/core/libs/nid/include/nid/NidResolver.hpp @@ -1,6 +1,7 @@ #ifndef NID_NIDRESOLVER_HPP #define NID_NIDRESOLVER_HPP +#include #include #include #include @@ -8,6 +9,10 @@ namespace Nid { +bool IsSceImport(const std::string& name); + +std::unordered_map ResolveImportNids(const std::vector& names, const std::unordered_set& excluded, const PatchOptions& options); + std::string ResolveOneName(const std::string& funcName); std::unordered_map ResolveNids(const std::vector& exportedNames, const std::string& libraryName, const std::unordered_set& excludedExports); diff --git a/core/libs/nid/include/nid/PeNidPatcher.hpp b/core/libs/nid/include/nid/PeNidPatcher.hpp index c674c3a80b..d9d14bda4b 100644 --- a/core/libs/nid/include/nid/PeNidPatcher.hpp +++ b/core/libs/nid/include/nid/PeNidPatcher.hpp @@ -39,7 +39,7 @@ struct PeSectionHeader { class PeNidPatcher final : public IBinaryPatcher { public: - void PatchNids(std::vector& binary, const std::string& libraryName, const std::unordered_set& excludedExports) const override; + void PatchNids(std::vector& binary, const std::string& libraryName, const std::unordered_set& excludedExports, const PatchOptions& options = {}) const override; }; } diff --git a/core/libs/nid/src/ElfNidPatcher.cpp b/core/libs/nid/src/ElfNidPatcher.cpp index 089bcd7107..a8e3f24f9a 100644 --- a/core/libs/nid/src/ElfNidPatcher.cpp +++ b/core/libs/nid/src/ElfNidPatcher.cpp @@ -354,7 +354,7 @@ std::vector CollectVerneedNameOffsets(const std::vector& elf, const std::string& libraryName, const std::unordered_set& excludedExports) const { +void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& libraryName, const std::unordered_set& excludedExports, const PatchOptions& options) const { using namespace Internal; if (elf.size() < sizeof(Elf64_Ehdr)) throw std::runtime_error("file too small"); @@ -370,11 +370,13 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& std::size_t sysvHashOffset = 0u, sysvHashSize = 0u; std::uint32_t sysvHashLink = 0u; std::size_t versymOffset = 0u; + bool hasDynamic = false; std::uint32_t dynSymSectionIndex = 0u; std::uint32_t gnuHashSymOffset = 0u; for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) { const std::size_t shOffset = static_cast(ehdr.e_shoff) + i * sizeof(Elf64_Shdr); const auto shdr = Read(elf, shOffset); + if (shdr.sh_type == kShtDynamic) hasDynamic = true; if (shdr.sh_type == kShtDynsym) { if (shdr.sh_entsize != sizeof(Elf64_Sym) || shdr.sh_size % sizeof(Elf64_Sym) || shdr.sh_link >= ehdr.e_shnum) throw std::runtime_error("invalid ELF dynsym layout"); @@ -398,6 +400,7 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& throw std::runtime_error("unsupported .gnu.version_d section: symbol version definitions cannot be preserved"); } + if (options.executable && dynSymOffset == 0u && !hasDynamic) return; if (dynSymOffset == 0u) throw std::runtime_error("no .dynsym section"); const std::size_t symCount = dynSymSize / sizeof(Elf64_Sym); @@ -437,19 +440,26 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& elf.begin() + static_cast(dynStrOffset + dynStrSize) ); + std::vector importedNames; std::vector exportedNames; for (std::size_t i = 1u; i < symCount; ++i) { const std::size_t symOffset = dynSymOffset + i * sizeof(Elf64_Sym); const auto sym = Read(elf, symOffset); if (sym.st_name == 0u) continue; const std::uint8_t binding = sym.st_info >> 4u; - if (binding == kStbLocal || sym.st_shndx == kShnUndef) continue; + if (binding == kStbLocal) continue; + if (sym.st_shndx == kShnUndef) { + const auto name = ReadCStr(origDynStr, sym.st_name); + if (!name.empty()) importedNames.push_back(name); + continue; + } const std::string symName = ReadCStr(origDynStr, sym.st_name); if (symName.empty()) continue; exportedNames.push_back(symName); } - const auto nidMap = ResolveNids(exportedNames, libraryName, excludedExports); + const auto nidMap = options.executable ? std::unordered_map{} : ResolveNids(exportedNames, libraryName, excludedExports); + const auto importMap = ResolveImportNids(importedNames, excludedExports, options); struct SymbolNameUse { std::size_t symIndex; @@ -481,14 +491,11 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& } else if (binding != kStbLocal && sym.st_shndx == kShnUndef) { const bool isUnversioned = versymOffset == 0u || (Read(elf, versymOffset + i * sizeof(std::uint16_t)) <= 1u); - const bool hasNidPostfix = symName.size() >= kNidPostfixLen && - symName.compare(symName.size() - kNidPostfixLen, kNidPostfixLen, kNidPostfix) == 0; - const bool hasScePrefix = symName.size() >= 3u && - std::tolower(static_cast(symName[0])) == 's' && - std::tolower(static_cast(symName[1])) == 'c' && - std::tolower(static_cast(symName[2])) == 'e'; - if (isUnversioned && (hasNidPostfix || hasScePrefix)) - newValue = ResolveOneName(symName); + if (options.executable || isUnversioned) { + newValue = importMap.at(symName); + if (options.executable && newValue != symName && versymOffset != 0u) + Write(elf, versymOffset + i * sizeof(std::uint16_t), std::uint16_t{1}); + } } uses.push_back(SymbolNameUse{i, sym.st_name, newValue}); @@ -538,11 +545,69 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& for (const auto offset : preservedOffsets) preservedToNewOffset.emplace_back(offset, valueOffsets.at(ReadCStr(origDynStr, offset))); + if (newDynStr.size() > dynStrSize && options.executable) { + if (ehdr.e_phentsize != sizeof(Elf64_Phdr) || ehdr.e_phnum == 0 || ehdr.e_phnum >= 0xfffdu) + throw std::runtime_error("invalid ELF program header table for string growth"); + RequireElfRange(elf, ehdr.e_phoff, std::uint64_t{ehdr.e_phnum} * sizeof(Elf64_Phdr), "program headers"); + const auto align = [](std::uint64_t value) { + if (value > UINT64_MAX - 4095u) throw std::runtime_error("ELF address overflow"); + return (value + 4095u) & ~std::uint64_t{4095u}; + }; + std::vector segments; + std::uint64_t virtualEnd = 0; + bool hasPhdr = false; + for (std::size_t index = 0; index < ehdr.e_phnum; ++index) { + const auto segment = Read(elf, ehdr.e_phoff + index * sizeof(Elf64_Phdr)); + segments.push_back(segment); + hasPhdr |= segment.p_type == 6; + if (segment.p_type != 1) continue; + if (segment.p_memsz > UINT64_MAX - segment.p_vaddr) throw std::runtime_error("ELF segment address overflow"); + virtualEnd = std::max(virtualEnd, segment.p_vaddr + segment.p_memsz); + } + if (!virtualEnd) throw std::runtime_error("ELF executable has no load segment"); + if (!hasPhdr) segments.insert(segments.begin(), Elf64_Phdr{6, 4, 0, 0, 0, 0, 0, 8}); + const auto fileOffset = align(elf.size()); + const auto address = align(virtualEnd); + const auto tableSize = (segments.size() + 1u) * sizeof(Elf64_Phdr); + const auto segmentSize = tableSize + newDynStr.size(); + if (segmentSize > UINT64_MAX - address || segmentSize > SIZE_MAX - fileOffset) throw std::runtime_error("ELF string segment overflow"); + for (auto& segment : segments) { + if (segment.p_type != 6) continue; + segment = {6, 4, fileOffset, address, address, tableSize, tableSize, 8}; + } + segments.push_back({1, 4, fileOffset, address, address, segmentSize, segmentSize, 4096}); + elf.resize(static_cast(fileOffset + segmentSize), 0); + for (std::size_t index = 0; index < segments.size(); ++index) + Write(elf, fileOffset + index * sizeof(Elf64_Phdr), segments[index]); + Write(elf, offsetof(Elf64_Ehdr, e_phoff), fileOffset); + Write(elf, offsetof(Elf64_Ehdr, e_phnum), static_cast(segments.size())); + dynStrOffset = static_cast(fileOffset + tableSize); + dynStrSize = newDynStr.size(); + const auto stringAddress = address + tableSize; + const auto sectionOffset = ehdr.e_shoff + dynStrSectionLink * sizeof(Elf64_Shdr); + auto strings = Read(elf, sectionOffset); + strings.sh_offset = dynStrOffset; + strings.sh_addr = stringAddress; + strings.sh_size = dynStrSize; + Write(elf, sectionOffset, strings); + bool foundAddress = false; + bool foundSize = false; + for (std::size_t index = 0; index < ehdr.e_shnum; ++index) { + const auto section = Read(elf, ehdr.e_shoff + index * sizeof(Elf64_Shdr)); + if (section.sh_type != kShtDynamic) continue; + RequireElfRange(elf, section.sh_offset, section.sh_size, "dynamic"); + for (std::uint64_t offset = 0; offset + sizeof(Elf64_Dyn) <= section.sh_size; offset += sizeof(Elf64_Dyn)) { + auto entry = Read(elf, section.sh_offset + offset); + if (entry.d_tag == kDtNull) break; + if (entry.d_tag == 5) { entry.d_val = stringAddress; foundAddress = true; } + if (entry.d_tag == 10) { entry.d_val = dynStrSize; foundSize = true; } + Write(elf, section.sh_offset + offset, entry); + } + } + if (!foundAddress || !foundSize) throw std::runtime_error("ELF executable lacks DT_STRTAB or DT_STRSZ"); + } if (newDynStr.size() > dynStrSize) { - throw std::runtime_error( - "rebuilt .dynstr (" + std::to_string(newDynStr.size()) + " bytes) exceeds original section size (" + - std::to_string(dynStrSize) + " bytes) - relinking with a larger section is required" - ); + throw std::runtime_error("rebuilt .dynstr exceeds original section size; relinking with a larger section is required"); } newDynStr.resize(dynStrSize, 0u); diff --git a/core/libs/nid/src/MachONidPatcher.cpp b/core/libs/nid/src/MachONidPatcher.cpp index 45bdee2735..71e8713f27 100644 --- a/core/libs/nid/src/MachONidPatcher.cpp +++ b/core/libs/nid/src/MachONidPatcher.cpp @@ -341,15 +341,6 @@ std::string StripUnderscore(const std::string& name) { return !name.empty() && name[0] == '_' ? name.substr(1) : name; } -bool IsGuestFacingImport(const std::string& name) { - using namespace Internal; - const bool hasNidPostfix = name.size() >= kNidPostfixLen && name.compare(name.size() - kNidPostfixLen, kNidPostfixLen, kNidPostfix) == 0; - const bool hasScePrefix = name.size() >= 3u && - std::tolower(static_cast(name[0])) == 's' && - std::tolower(static_cast(name[1])) == 'c' && - std::tolower(static_cast(name[2])) == 'e'; - return hasNidPostfix || hasScePrefix; -} std::vector DylibOrdinalsArePrx(const std::vector& binary, const Image& image) { std::vector result; @@ -364,11 +355,48 @@ std::vector DylibOrdinalsArePrx(const std::vector& binary, c return result; } -void PatchChainedImports(std::vector& binary, const Image& image) { +void WriteLinkeditData(std::vector& binary, const Image& image, const TrieLocation& location, const std::vector& trie) { + const auto oldOffset = Read(binary, location.OffsetField); + const auto oldSize = Read(binary, location.SizeField); + if (trie.size() <= oldSize) { + std::copy(trie.begin(), trie.end(), binary.begin() + oldOffset); + std::fill(binary.begin() + oldOffset + static_cast(trie.size()), binary.begin() + oldOffset + oldSize, 0); + Write(binary, location.SizeField, static_cast(trie.size())); + return; + } + std::optional linkedit; + for (const auto offset : image.Commands) { + const auto cmd = Read(binary, offset).Cmd; + if (cmd == kLcCodeSignature) throw std::runtime_error("signed Mach-O images cannot grow linkedit data"); + if (cmd != kLcSegment64) continue; + const auto segment = Read(binary, offset); + if (std::strncmp(segment.Name, "__LINKEDIT", sizeof(segment.Name)) == 0) linkedit = offset; + } + if (!linkedit) throw std::runtime_error("Mach-O image has no __LINKEDIT segment"); + auto segment = Read(binary, *linkedit); + if (segment.FileOffset + segment.FileSize != binary.size()) throw std::runtime_error("__LINKEDIT does not end the file"); + const auto newOffset = (binary.size() + 7) & ~std::size_t{7}; + if (newOffset + trie.size() > 0xffffffffu) throw std::runtime_error("linkedit data offset overflow"); + binary.resize(newOffset, 0); + binary.insert(binary.end(), trie.begin(), trie.end()); + std::fill(binary.begin() + oldOffset, binary.begin() + oldOffset + oldSize, 0); + segment.FileSize = binary.size() - segment.FileOffset; + segment.VmSize = std::max(segment.VmSize, (segment.FileSize + kPageSize - 1) & ~(kPageSize - 1)); + Write(binary, *linkedit, segment); + Write(binary, location.OffsetField, static_cast(newOffset)); + Write(binary, location.SizeField, static_cast(trie.size())); +} + +void PatchChainedImports(std::vector& binary, const Image& image, const std::unordered_set& excluded, const PatchOptions& options) { + std::size_t commandOffset = 0; std::optional fixups; for (const auto offset : image.Commands) { const auto cmd = Read(binary, offset).Cmd; - if (cmd == kLcDyldChainedFixups) fixups = Read(binary, offset); + if (cmd == kLcDyldChainedFixups) { + fixups = Read(binary, offset); + commandOffset = offset; + } + if (options.executable && cmd == kLcCodeSignature) throw std::runtime_error("remove the Mach-O code signature before patching and sign again afterwards"); if ((cmd == kLcDyldInfo || cmd == kLcDyldInfoOnly)) { const auto info = Read(binary, offset); if (info.BindSize != 0 || info.LazyBindSize != 0 || info.WeakBindSize != 0) throw std::runtime_error("Mach-O bind opcodes are not supported; link with chained fixups"); @@ -400,61 +428,72 @@ void PatchChainedImports(std::vector& binary, const Image& image) imports.push_back({raw >> 9, raw & 0xff}); } } - std::set nameOffsets; - for (const auto& item : imports) nameOffsets.insert(item.NameOffset); - std::set rewritten; + std::vector names; + std::vector originalNames; + std::vector selected; for (const auto& item : imports) { - const bool special = header.ImportsFormat == kImportFormatAddend64 ? item.Ordinal >= 0xfff0 : item.Ordinal >= 0xf0; - if (item.Ordinal == 0 || special || item.Ordinal > prx.size() || !prx[item.Ordinal - 1]) continue; - if (rewritten.contains(item.NameOffset)) continue; const auto at = symbols + item.NameOffset; if (at >= symbolsEnd) throw std::runtime_error("chained import name out of bounds"); const auto name = Internal::ReadCStr(binary, at); if (at + name.size() >= symbolsEnd) throw std::runtime_error("unterminated chained import name"); const auto bare = StripUnderscore(name); - if (bare == name || !IsGuestFacingImport(bare)) continue; - const auto patched = "_" + ResolveOneName(bare); - if (patched.size() > name.size()) throw std::runtime_error("import NID longer than original name: " + name); - const auto inside = nameOffsets.upper_bound(item.NameOffset); - if (inside != nameOffsets.end() && *inside <= item.NameOffset + name.size()) throw std::runtime_error("chained import names share storage: " + name); - std::memcpy(binary.data() + at, patched.data(), patched.size()); - std::fill(binary.begin() + static_cast(at + patched.size()), binary.begin() + static_cast(at + name.size() + 1), 0); - rewritten.insert(item.NameOffset); + const bool special = header.ImportsFormat == kImportFormatAddend64 ? item.Ordinal >= 0xfff0 : item.Ordinal >= 0xf0; + const bool eligible = options.executable || (bare != name && item.Ordinal != 0 && !special && item.Ordinal <= prx.size() && prx[item.Ordinal - 1]); + originalNames.push_back(name); + selected.push_back(eligible); + if (eligible) names.push_back(bare); } -} - -void WriteTrie(std::vector& binary, const Image& image, const TrieLocation& location, const std::vector& trie) { - const auto oldOffset = Read(binary, location.OffsetField); - const auto oldSize = Read(binary, location.SizeField); - if (trie.size() <= oldSize) { - std::copy(trie.begin(), trie.end(), binary.begin() + oldOffset); - std::fill(binary.begin() + oldOffset + static_cast(trie.size()), binary.begin() + oldOffset + oldSize, 0); - Write(binary, location.SizeField, static_cast(trie.size())); + const auto mapping = ResolveImportNids(names, excluded, options); + if (!options.executable) { + std::set nameOffsets; + for (const auto& item : imports) nameOffsets.insert(item.NameOffset); + std::set rewritten; + for (const auto& item : imports) { + const bool special = header.ImportsFormat == kImportFormatAddend64 ? item.Ordinal >= 0xfff0 : item.Ordinal >= 0xf0; + if (item.Ordinal == 0 || special || item.Ordinal > prx.size() || !prx[item.Ordinal - 1]) continue; + if (rewritten.contains(item.NameOffset)) continue; + const auto at = symbols + item.NameOffset; + if (at >= symbolsEnd) throw std::runtime_error("chained import name out of bounds"); + const auto name = Internal::ReadCStr(binary, at); + if (at + name.size() >= symbolsEnd) throw std::runtime_error("unterminated chained import name"); + const auto bare = StripUnderscore(name); + if (bare == name) continue; + const auto patched = "_" + mapping.at(bare); + if (patched.size() > name.size()) throw std::runtime_error("import NID longer than original name: " + name); + const auto inside = nameOffsets.upper_bound(item.NameOffset); + if (inside != nameOffsets.end() && *inside <= item.NameOffset + name.size()) throw std::runtime_error("chained import names share storage: " + name); + std::memcpy(binary.data() + at, patched.data(), patched.size()); + std::fill(binary.begin() + static_cast(at + patched.size()), binary.begin() + static_cast(at + name.size() + 1), 0); + rewritten.insert(item.NameOffset); + } return; } - std::optional linkedit; - for (const auto offset : image.Commands) { - const auto cmd = Read(binary, offset).Cmd; - if (cmd == kLcCodeSignature) throw std::runtime_error("signed Mach-O images cannot grow their export trie"); - if (cmd != kLcSegment64) continue; - const auto segment = Read(binary, offset); - if (std::strncmp(segment.Name, "__LINKEDIT", sizeof(segment.Name)) == 0) linkedit = offset; + if (imports.empty()) return; + std::vector data(binary.begin() + base, binary.begin() + symbolsEnd); + const auto newSymbols = data.size(); + if (newSymbols > 0xffffffffu) throw std::runtime_error("chained symbol offset overflow"); + Write(data, offsetof(ChainedFixupsHeader, SymbolsOffset), static_cast(newSymbols)); + for (std::size_t index = 0; index < imports.size(); ++index) { + const auto& original = originalNames[index]; + const auto name = selected[index] ? (original.starts_with("_") ? "_" : "") + mapping.at(StripUnderscore(original)) : original; + const auto offset = data.size() - newSymbols; + const auto at = header.ImportsOffset + index * entrySize; + if (header.ImportsFormat == kImportFormatAddend64) { + if (offset > 0xffffffffu) throw std::runtime_error("chained symbol offset overflow"); + const auto raw = Read(data, at); + Write(data, at, (raw & 0xffffffffu) | (std::uint64_t{offset} << 32)); + } else { + if (offset > 0x7fffffu) throw std::runtime_error("chained symbol offset overflow"); + const auto raw = Read(data, at); + Write(data, at, (raw & 0x1ffu) | (static_cast(offset) << 9)); + } + data.insert(data.end(), name.begin(), name.end()); + data.push_back(0); } - if (!linkedit) throw std::runtime_error("Mach-O image has no __LINKEDIT segment"); - auto segment = Read(binary, *linkedit); - if (segment.FileOffset + segment.FileSize != binary.size()) throw std::runtime_error("__LINKEDIT does not end the file"); - const auto newOffset = (binary.size() + 7) & ~std::size_t{7}; - if (newOffset + trie.size() > 0xffffffffu) throw std::runtime_error("export trie offset overflow"); - binary.resize(newOffset, 0); - binary.insert(binary.end(), trie.begin(), trie.end()); - std::fill(binary.begin() + oldOffset, binary.begin() + oldOffset + oldSize, 0); - segment.FileSize = binary.size() - segment.FileOffset; - segment.VmSize = std::max(segment.VmSize, (segment.FileSize + kPageSize - 1) & ~(kPageSize - 1)); - Write(binary, *linkedit, segment); - Write(binary, location.OffsetField, static_cast(newOffset)); - Write(binary, location.SizeField, static_cast(trie.size())); + WriteLinkeditData(binary, image, {commandOffset + 8, commandOffset + 12}, data); } + } std::vector ReadMachOExports(const std::vector& binary) { @@ -464,8 +503,12 @@ std::vector ReadMachOExports(const std::vector& binar return ParseTrie(binary, Read(binary, location->OffsetField), Read(binary, location->SizeField)); } -void MachONidPatcher::PatchNids(std::vector& binary, const std::string& libraryName, const std::unordered_set& excludedExports) const { +void MachONidPatcher::PatchNids(std::vector& binary, const std::string& libraryName, const std::unordered_set& excludedExports, const PatchOptions& options) const { const auto image = ParseImage(binary); + if (options.executable) { + PatchChainedImports(binary, image, excludedExports, options); + return; + } const auto location = FindTrie(binary, image); if (!location) throw std::runtime_error("no export trie"); auto exports = ParseTrie(binary, Read(binary, location->OffsetField), Read(binary, location->SizeField)); @@ -486,8 +529,8 @@ void MachONidPatcher::PatchNids(std::vector& binary, const std::st if (found == nidMap.end()) throw std::runtime_error("symbol not in nid map: " + name); exports[index].Name = "_" + found->second; } - WriteTrie(binary, image, *location, TrieBuilder(exports).Build()); - PatchChainedImports(binary, ParseImage(binary)); + WriteLinkeditData(binary, image, *location, TrieBuilder(exports).Build()); + PatchChainedImports(binary, ParseImage(binary), excludedExports, options); } } diff --git a/core/libs/nid/src/NidResolver.cpp b/core/libs/nid/src/NidResolver.cpp index 200565a6a7..aefa72ee8c 100644 --- a/core/libs/nid/src/NidResolver.cpp +++ b/core/libs/nid/src/NidResolver.cpp @@ -3,21 +3,32 @@ #include #include #include +#include #include #include #include namespace Nid { +bool IsSceImport(const std::string& name) { + return name.ends_with(Internal::kNidPostfix) || (name.size() >= 3 && + std::tolower(static_cast(name[0])) == 's' && + std::tolower(static_cast(name[1])) == 'c' && + std::tolower(static_cast(name[2])) == 'e'); +} + +std::unordered_map ResolveImportNids(const std::vector& names, const std::unordered_set& excluded, const PatchOptions& options) { + const std::unordered_set unique(names.begin(), names.end()); + auto result = ResolveNids(std::vector(unique.begin(), unique.end()), "", excluded); + for (const auto& name : unique) { + if ((!options.executable || options.onlySce) && !IsSceImport(name)) result.at(name) = name; + else if (!options.executable) result.at(name) = ResolveOneName(name); + } + return result; +} + std::string ResolveOneName(const std::string& funcName) { - using namespace Internal; - - if (IsNidNoPatchCut(funcName)) - return StripNidNoPatchCut(funcName); - if (IsNidNoPatch(funcName)) - return funcName; - - return ComputeNid(StripNidPostfix(funcName), ""); + return ResolveNids({funcName}, "", {}).at(funcName); } std::unordered_map ResolveNids(const std::vector& exportedNames, const std::string& libraryName, const std::unordered_set& excludedExports) { diff --git a/core/libs/nid/src/PeNidPatcher.cpp b/core/libs/nid/src/PeNidPatcher.cpp index 58fb8ade0a..638f3bb29e 100644 --- a/core/libs/nid/src/PeNidPatcher.cpp +++ b/core/libs/nid/src/PeNidPatcher.cpp @@ -36,7 +36,7 @@ std::size_t RvaToOffset(const std::vector& pe, std::uint32_t rva, } -void PeNidPatcher::PatchNids(std::vector& pe, const std::string& libraryName, const std::unordered_set& excludedExports) const { +void PeNidPatcher::PatchNids(std::vector& pe, const std::string& libraryName, const std::unordered_set& excludedExports, const PatchOptions& options) const { using namespace Internal; if (pe.size() < 0x40) throw std::runtime_error("file too small"); @@ -54,7 +54,7 @@ void PeNidPatcher::PatchNids(std::vector& pe, const std::string& l const std::size_t sectionTableOffset = coffHeaderOffset + 20u + sizeOfOptionalHeader; const auto symbolTable = Read(pe, coffHeaderOffset + 8u); const auto symbolCount = Read(pe, coffHeaderOffset + 12u); - for (std::uint16_t i = 0; i < numberOfSections; ++i) { + for (std::uint16_t i = 0; !options.executable && i < numberOfSections; ++i) { const auto offset = sectionTableOffset + i * sizeof(PeSectionHeader); auto section = Read(pe, offset); if (pe[offset] != '/' || !symbolTable) continue; @@ -79,159 +79,162 @@ void PeNidPatcher::PatchNids(std::vector& pe, const std::string& l throw std::runtime_error("unsupported optional header magic"); } - const auto exportDir = Read(pe, dataDirectoryOffset); - if (exportDir.VirtualAddress == 0u) throw std::runtime_error("no export directory"); + if (!options.executable) { + const auto exportDir = Read(pe, dataDirectoryOffset); + if (exportDir.VirtualAddress == 0u) throw std::runtime_error("no export directory"); - const std::size_t exportDirOffset = RvaToOffset(pe, exportDir.VirtualAddress, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); - const auto exportTable = Read(pe, exportDirOffset); + const std::size_t exportDirOffset = RvaToOffset(pe, exportDir.VirtualAddress, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); + const auto exportTable = Read(pe, exportDirOffset); - if (exportTable.NumberOfNames == 0u) throw std::runtime_error("no exported names"); + if (exportTable.NumberOfNames == 0u) throw std::runtime_error("no exported names"); - const std::size_t namesArrayOffset = RvaToOffset(pe, exportTable.AddressOfNames, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); - const std::size_t ordinalsArrayOffset = RvaToOffset(pe, exportTable.AddressOfNameOrdinals, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); + const std::size_t namesArrayOffset = RvaToOffset(pe, exportTable.AddressOfNames, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); + const std::size_t ordinalsArrayOffset = RvaToOffset(pe, exportTable.AddressOfNameOrdinals, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); - std::vector nameRvas(exportTable.NumberOfNames); - std::vector names(exportTable.NumberOfNames); - for (std::uint32_t i = 0u; i < exportTable.NumberOfNames; ++i) { - const auto nameRva = Read(pe, namesArrayOffset + i * 4u); - const std::size_t nameOffset = RvaToOffset(pe, nameRva, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); - const std::string name = ReadCStr(pe, nameOffset); - if (name.empty()) throw std::runtime_error("empty exported name"); - nameRvas[i] = nameRva; - names[i] = name; - } - - const auto nidMap = ResolveNids(names, libraryName, excludedExports); - - const std::size_t edataSectionOffset = FindSectionOffsetByRva(pe, exportDir.VirtualAddress, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); - const auto edataSection = Read(pe, edataSectionOffset); - - const auto fileAlignment = Read(pe, optionalHeaderOffset + 36u); - if (fileAlignment == 0u) throw std::runtime_error("invalid file alignment"); - - std::uint32_t stringsRegionStart = std::numeric_limits::max(); - for (std::uint32_t i = 0u; i < exportTable.NumberOfNames; ++i) { - const std::uint32_t rvaFromSection = nameRvas[i] - edataSection.VirtualAddress; - if (rvaFromSection < stringsRegionStart) stringsRegionStart = rvaFromSection; - } - if (stringsRegionStart == std::numeric_limits::max()) - throw std::runtime_error("could not determine strings region start"); - - std::uint32_t rawLimit = ((edataSection.SizeOfRawData + fileAlignment - 1u) / fileAlignment) * fileAlignment; - const auto dllName = ReadCStr(pe, RvaToOffset(pe, exportTable.Name, peHeaderOffset, numberOfSections, sizeOfOptionalHeader)); - const bool repackDllName = exportTable.Name >= edataSection.VirtualAddress && - exportTable.Name - edataSection.VirtualAddress < rawLimit; - if (repackDllName) stringsRegionStart = std::min(stringsRegionStart, exportTable.Name - edataSection.VirtualAddress); - struct Forwarder { - std::size_t EntryOffset; - std::string Target; - }; - std::vector forwarders; - std::vector directRvas; - const auto functionsArrayOffset = RvaToOffset(pe, exportTable.AddressOfFunctions, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); - for (std::uint32_t index = 0u; index < exportTable.NumberOfFunctions; ++index) { - const auto entryOffset = functionsArrayOffset + index * sizeof(std::uint32_t); - const auto rva = Read(pe, entryOffset); - if (rva < exportDir.VirtualAddress || std::uint64_t{rva} - exportDir.VirtualAddress >= exportDir.Size) { - directRvas.push_back(rva); - continue; + std::vector nameRvas(exportTable.NumberOfNames); + std::vector names(exportTable.NumberOfNames); + for (std::uint32_t i = 0u; i < exportTable.NumberOfNames; ++i) { + const auto nameRva = Read(pe, namesArrayOffset + i * 4u); + const std::size_t nameOffset = RvaToOffset(pe, nameRva, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); + const std::string name = ReadCStr(pe, nameOffset); + if (name.empty()) throw std::runtime_error("empty exported name"); + nameRvas[i] = nameRva; + names[i] = name; } - const auto target = ReadCStr(pe, RvaToOffset(pe, rva, peHeaderOffset, numberOfSections, sizeOfOptionalHeader)); - if (target.size() + 1u > exportDir.Size - (rva - exportDir.VirtualAddress)) - throw std::runtime_error("forwarder string outside export directory"); - forwarders.push_back({entryOffset, target}); - stringsRegionStart = std::min(stringsRegionStart, rva - edataSection.VirtualAddress); - } - for (const auto rva : directRvas) { - if (rva >= edataSection.VirtualAddress && rva - edataSection.VirtualAddress >= stringsRegionStart) - rawLimit = std::min(rawLimit, rva - edataSection.VirtualAddress); - } - const auto reserveTable = [&](std::uint32_t rva, std::uint64_t size) { - if (rva < edataSection.VirtualAddress || rva - edataSection.VirtualAddress >= rawLimit) return; - const auto end = std::uint64_t{rva - edataSection.VirtualAddress} + size; - if (end > rawLimit) throw std::runtime_error("export table outside edata section"); - stringsRegionStart = std::max(stringsRegionStart, static_cast(end)); - }; - reserveTable(exportDir.VirtualAddress, sizeof(PeExportDirectory)); - reserveTable(exportTable.AddressOfFunctions, std::uint64_t{exportTable.NumberOfFunctions} * 4u); - reserveTable(exportTable.AddressOfNames, std::uint64_t{exportTable.NumberOfNames} * 4u); - reserveTable(exportTable.AddressOfNameOrdinals, std::uint64_t{exportTable.NumberOfNames} * 2u); - std::vector finalNames(exportTable.NumberOfNames); - for (std::uint32_t i = 0u; i < exportTable.NumberOfNames; ++i) { - const auto it = nidMap.find(names[i]); - if (it == nidMap.end()) throw std::runtime_error("symbol not in nid map: " + names[i]); - finalNames[i] = it->second; - } + const auto nidMap = ResolveNids(names, libraryName, excludedExports); - std::vector strings; - std::unordered_map stringRvas; - const auto storeString = [&](const std::string& name) { - const auto existing = stringRvas.find(name); - if (existing != stringRvas.end()) return existing->second; - const auto rva = edataSection.VirtualAddress + stringsRegionStart + static_cast(strings.size()); - stringRvas.emplace(name, rva); - strings.insert(strings.end(), name.begin(), name.end()); - strings.push_back(0u); - return rva; - }; - std::vector forwarderRvas; - for (const auto& forwarder : forwarders) forwarderRvas.push_back(storeString(forwarder.Target)); - if (!forwarders.empty() && (std::uint64_t{edataSection.VirtualAddress} + stringsRegionStart < exportDir.VirtualAddress || - std::uint64_t{edataSection.VirtualAddress} + stringsRegionStart + strings.size() > std::uint64_t{exportDir.VirtualAddress} + exportDir.Size)) - throw std::runtime_error("not enough export directory space to repack forwarders"); - const auto dllNameRva = repackDllName ? storeString(dllName) : exportTable.Name; - std::vector finalNameRvas; - for (const auto& name : finalNames) finalNameRvas.push_back(storeString(name)); - const auto requiredSize = std::uint64_t{stringsRegionStart} + strings.size(); - if (requiredSize > rawLimit) - throw std::runtime_error("not enough raw space in edata section to repack export names"); - const auto directoryEnd = std::max(std::uint64_t{exportDir.VirtualAddress} + exportDir.Size, - std::uint64_t{edataSection.VirtualAddress} + requiredSize); - for (const auto rva : directRvas) { - if (rva != 0u && rva >= exportDir.VirtualAddress && std::uint64_t{rva} < directoryEnd) - throw std::runtime_error("repacked export directory would cover a direct export"); - } - const auto directorySize = directoryEnd - exportDir.VirtualAddress; - if (directorySize > std::numeric_limits::max()) throw std::runtime_error("export directory too large"); - const auto stringFileOffset = static_cast(edataSection.PointerToRawData) + stringsRegionStart; - if (stringFileOffset > pe.size() || strings.size() > pe.size() - stringFileOffset) - throw std::runtime_error("repack write out of file bounds"); - std::memcpy(pe.data() + stringFileOffset, strings.data(), strings.size()); - for (std::size_t index = 0u; index < finalNameRvas.size(); ++index) - Write(pe, namesArrayOffset + index * 4u, finalNameRvas[index]); - for (std::size_t index = 0u; index < forwarders.size(); ++index) - Write(pe, forwarders[index].EntryOffset, forwarderRvas[index]); - Write(pe, exportDirOffset + offsetof(PeExportDirectory, Name), dllNameRva); - Write(pe, dataDirectoryOffset + offsetof(PeDataDirectory, Size), static_cast(directorySize)); + const std::size_t edataSectionOffset = FindSectionOffsetByRva(pe, exportDir.VirtualAddress, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); + const auto edataSection = Read(pe, edataSectionOffset); - const std::uint32_t newVirtualSize = std::max(edataSection.VirtualSize, static_cast(requiredSize)); - auto patchedSection = edataSection; - patchedSection.VirtualSize = newVirtualSize; - Write(pe, edataSectionOffset, patchedSection); + const auto fileAlignment = Read(pe, optionalHeaderOffset + 36u); + if (fileAlignment == 0u) throw std::runtime_error("invalid file alignment"); - struct ExportName { - std::string Name; - std::uint32_t Rva; - std::uint16_t Ordinal; - }; + std::uint32_t stringsRegionStart = std::numeric_limits::max(); + for (std::uint32_t i = 0u; i < exportTable.NumberOfNames; ++i) { + const std::uint32_t rvaFromSection = nameRvas[i] - edataSection.VirtualAddress; + if (rvaFromSection < stringsRegionStart) stringsRegionStart = rvaFromSection; + } + if (stringsRegionStart == std::numeric_limits::max()) + throw std::runtime_error("could not determine strings region start"); + + std::uint32_t rawLimit = ((edataSection.SizeOfRawData + fileAlignment - 1u) / fileAlignment) * fileAlignment; + const auto dllName = ReadCStr(pe, RvaToOffset(pe, exportTable.Name, peHeaderOffset, numberOfSections, sizeOfOptionalHeader)); + const bool repackDllName = exportTable.Name >= edataSection.VirtualAddress && + exportTable.Name - edataSection.VirtualAddress < rawLimit; + if (repackDllName) stringsRegionStart = std::min(stringsRegionStart, exportTable.Name - edataSection.VirtualAddress); + struct Forwarder { + std::size_t EntryOffset; + std::string Target; + }; + std::vector forwarders; + std::vector directRvas; + const auto functionsArrayOffset = RvaToOffset(pe, exportTable.AddressOfFunctions, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); + for (std::uint32_t index = 0u; index < exportTable.NumberOfFunctions; ++index) { + const auto entryOffset = functionsArrayOffset + index * sizeof(std::uint32_t); + const auto rva = Read(pe, entryOffset); + if (rva < exportDir.VirtualAddress || std::uint64_t{rva} - exportDir.VirtualAddress >= exportDir.Size) { + directRvas.push_back(rva); + continue; + } + const auto target = ReadCStr(pe, RvaToOffset(pe, rva, peHeaderOffset, numberOfSections, sizeOfOptionalHeader)); + if (target.size() + 1u > exportDir.Size - (rva - exportDir.VirtualAddress)) + throw std::runtime_error("forwarder string outside export directory"); + forwarders.push_back({entryOffset, target}); + stringsRegionStart = std::min(stringsRegionStart, rva - edataSection.VirtualAddress); + } + for (const auto rva : directRvas) { + if (rva >= edataSection.VirtualAddress && rva - edataSection.VirtualAddress >= stringsRegionStart) + rawLimit = std::min(rawLimit, rva - edataSection.VirtualAddress); + } + const auto reserveTable = [&](std::uint32_t rva, std::uint64_t size) { + if (rva < edataSection.VirtualAddress || rva - edataSection.VirtualAddress >= rawLimit) return; + const auto end = std::uint64_t{rva - edataSection.VirtualAddress} + size; + if (end > rawLimit) throw std::runtime_error("export table outside edata section"); + stringsRegionStart = std::max(stringsRegionStart, static_cast(end)); + }; + reserveTable(exportDir.VirtualAddress, sizeof(PeExportDirectory)); + reserveTable(exportTable.AddressOfFunctions, std::uint64_t{exportTable.NumberOfFunctions} * 4u); + reserveTable(exportTable.AddressOfNames, std::uint64_t{exportTable.NumberOfNames} * 4u); + reserveTable(exportTable.AddressOfNameOrdinals, std::uint64_t{exportTable.NumberOfNames} * 2u); + + std::vector finalNames(exportTable.NumberOfNames); + for (std::uint32_t i = 0u; i < exportTable.NumberOfNames; ++i) { + const auto it = nidMap.find(names[i]); + if (it == nidMap.end()) throw std::runtime_error("symbol not in nid map: " + names[i]); + finalNames[i] = it->second; + } + + std::vector strings; + std::unordered_map stringRvas; + const auto storeString = [&](const std::string& name) { + const auto existing = stringRvas.find(name); + if (existing != stringRvas.end()) return existing->second; + const auto rva = edataSection.VirtualAddress + stringsRegionStart + static_cast(strings.size()); + stringRvas.emplace(name, rva); + strings.insert(strings.end(), name.begin(), name.end()); + strings.push_back(0u); + return rva; + }; + std::vector forwarderRvas; + for (const auto& forwarder : forwarders) forwarderRvas.push_back(storeString(forwarder.Target)); + if (!forwarders.empty() && (std::uint64_t{edataSection.VirtualAddress} + stringsRegionStart < exportDir.VirtualAddress || + std::uint64_t{edataSection.VirtualAddress} + stringsRegionStart + strings.size() > std::uint64_t{exportDir.VirtualAddress} + exportDir.Size)) + throw std::runtime_error("not enough export directory space to repack forwarders"); + const auto dllNameRva = repackDllName ? storeString(dllName) : exportTable.Name; + std::vector finalNameRvas; + for (const auto& name : finalNames) finalNameRvas.push_back(storeString(name)); + const auto requiredSize = std::uint64_t{stringsRegionStart} + strings.size(); + if (requiredSize > rawLimit) + throw std::runtime_error("not enough raw space in edata section to repack export names"); + const auto directoryEnd = std::max(std::uint64_t{exportDir.VirtualAddress} + exportDir.Size, + std::uint64_t{edataSection.VirtualAddress} + requiredSize); + for (const auto rva : directRvas) { + if (rva != 0u && rva >= exportDir.VirtualAddress && std::uint64_t{rva} < directoryEnd) + throw std::runtime_error("repacked export directory would cover a direct export"); + } + const auto directorySize = directoryEnd - exportDir.VirtualAddress; + if (directorySize > std::numeric_limits::max()) throw std::runtime_error("export directory too large"); + const auto stringFileOffset = static_cast(edataSection.PointerToRawData) + stringsRegionStart; + if (stringFileOffset > pe.size() || strings.size() > pe.size() - stringFileOffset) + throw std::runtime_error("repack write out of file bounds"); + std::memcpy(pe.data() + stringFileOffset, strings.data(), strings.size()); + for (std::size_t index = 0u; index < finalNameRvas.size(); ++index) + Write(pe, namesArrayOffset + index * 4u, finalNameRvas[index]); + for (std::size_t index = 0u; index < forwarders.size(); ++index) + Write(pe, forwarders[index].EntryOffset, forwarderRvas[index]); + Write(pe, exportDirOffset + offsetof(PeExportDirectory, Name), dllNameRva); + Write(pe, dataDirectoryOffset + offsetof(PeDataDirectory, Size), static_cast(directorySize)); + + const std::uint32_t newVirtualSize = std::max(edataSection.VirtualSize, static_cast(requiredSize)); + auto patchedSection = edataSection; + patchedSection.VirtualSize = newVirtualSize; + Write(pe, edataSectionOffset, patchedSection); + + struct ExportName { + std::string Name; + std::uint32_t Rva; + std::uint16_t Ordinal; + }; + + std::vector exportNames; + exportNames.reserve(exportTable.NumberOfNames); + for (std::size_t i = 0; i < exportTable.NumberOfNames; ++i) { + const auto nameRva = Read(pe, namesArrayOffset + i * sizeof(std::uint32_t)); + const auto ordinal = Read(pe, ordinalsArrayOffset + i * sizeof(std::uint16_t)); + if (ordinal >= exportTable.NumberOfFunctions) + throw std::runtime_error("export name ordinal out of bounds"); + const auto nameOffset = RvaToOffset(pe, nameRva, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); + exportNames.push_back({ReadCStr(pe, nameOffset), nameRva, ordinal}); + } + std::sort(exportNames.begin(), exportNames.end(), [](const ExportName& left, const ExportName& right) { return left.Name < right.Name; }); + for (std::size_t i = 0; i < exportNames.size(); ++i) { + if (i != 0 && exportNames[i - 1].Name == exportNames[i].Name) + throw std::runtime_error("duplicate patched export name: " + exportNames[i].Name); + Write(pe, namesArrayOffset + i * sizeof(std::uint32_t), exportNames[i].Rva); + Write(pe, ordinalsArrayOffset + i * sizeof(std::uint16_t), exportNames[i].Ordinal); + } - std::vector exportNames; - exportNames.reserve(exportTable.NumberOfNames); - for (std::size_t i = 0; i < exportTable.NumberOfNames; ++i) { - const auto nameRva = Read(pe, namesArrayOffset + i * sizeof(std::uint32_t)); - const auto ordinal = Read(pe, ordinalsArrayOffset + i * sizeof(std::uint16_t)); - if (ordinal >= exportTable.NumberOfFunctions) - throw std::runtime_error("export name ordinal out of bounds"); - const auto nameOffset = RvaToOffset(pe, nameRva, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); - exportNames.push_back({ReadCStr(pe, nameOffset), nameRva, ordinal}); - } - std::sort(exportNames.begin(), exportNames.end(), [](const ExportName& left, const ExportName& right) { return left.Name < right.Name; }); - for (std::size_t i = 0; i < exportNames.size(); ++i) { - if (i != 0 && exportNames[i - 1].Name == exportNames[i].Name) - throw std::runtime_error("duplicate patched export name: " + exportNames[i].Name); - Write(pe, namesArrayOffset + i * sizeof(std::uint32_t), exportNames[i].Rva); - Write(pe, ordinalsArrayOffset + i * sizeof(std::uint16_t), exportNames[i].Ordinal); } constexpr std::size_t kImportDirIndex = 1u; @@ -250,6 +253,20 @@ void PeNidPatcher::PatchNids(std::vector& pe, const std::string& l std::uint32_t FirstThunk; }; + if (options.executable) { + for (const auto index : {4u, 11u, 13u}) { + const auto directory = Read(pe, dataDirectoryOffset + index * sizeof(PeDataDirectory)); + if (directory.VirtualAddress || directory.Size) throw std::runtime_error("signed, bound or delay-loaded PE imports are not supported in --exec mode"); + } + } + struct ImportUse { + std::size_t NameOffset; + std::size_t LookupOffset; + std::size_t AddressOffset; + std::string Name; + }; + std::vector imports; + std::vector importNames; std::size_t importDescOffset = RvaToOffset(pe, importDir.VirtualAddress, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); while (true) { @@ -286,19 +303,9 @@ void PeNidPatcher::PatchNids(std::vector& pe, const std::string& l const std::string funcName = ReadCStr(pe, hintNameOffset + 2u); if (!funcName.empty()) { - const bool hasNidPostfix = funcName.size() >= kNidPostfixLen && - funcName.compare(funcName.size() - kNidPostfixLen, kNidPostfixLen, kNidPostfix) == 0; - const bool hasScePrefix = funcName.size() >= 3u && - std::tolower(static_cast(funcName[0])) == 's' && - std::tolower(static_cast(funcName[1])) == 'c' && - std::tolower(static_cast(funcName[2])) == 'e'; - if (hasNidPostfix || hasScePrefix) { - const std::string nid = ResolveOneName(funcName); - if (nid.size() > funcName.size()) - throw std::runtime_error("import NID longer than original name: " + funcName); - std::memcpy(pe.data() + hintNameOffset + 2u, nid.data(), nid.size()); - pe[hintNameOffset + 2u + nid.size()] = 0u; - } + const auto addressOffset = RvaToOffset(pe, desc.FirstThunk, peHeaderOffset, numberOfSections, sizeOfOptionalHeader) + thunkOffset - RvaToOffset(pe, thunkRva, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); + imports.push_back({hintNameOffset + 2u, thunkOffset, addressOffset, funcName}); + importNames.push_back(funcName); } } @@ -307,6 +314,72 @@ void PeNidPatcher::PatchNids(std::vector& pe, const std::string& l importDescOffset += sizeof(PeImportDescriptor); } + const auto importMap = ResolveImportNids(importNames, excludedExports, options); + if (!options.executable) { + for (const auto& item : imports) { + const auto& name = importMap.at(item.Name); + if (name.size() > item.Name.size()) throw std::runtime_error("import NID longer than original name: " + item.Name); + std::memcpy(pe.data() + item.NameOffset, name.c_str(), name.size() + 1); + } + return; + } + std::vector strings; + std::unordered_map stringOffsets; + for (const auto& item : imports) { + const auto& name = importMap.at(item.Name); + if (name == item.Name || stringOffsets.contains(name)) continue; + if (strings.size() & 1u) strings.push_back(0); + if (strings.size() > 0xffffffffu - name.size() - 3u) throw std::runtime_error("PE import string table overflow"); + stringOffsets.emplace(name, static_cast(strings.size())); + strings.insert(strings.end(), {0, 0}); + strings.insert(strings.end(), name.begin(), name.end()); + strings.push_back(0); + } + if (strings.empty()) return; + const auto fileAlignment = Read(pe, optionalHeaderOffset + 36u); + const auto sectionAlignment = Read(pe, optionalHeaderOffset + 32u); + const auto align = [](std::uint64_t value, std::uint32_t alignment) { + if (!alignment || (alignment & (alignment - 1u))) throw std::runtime_error("invalid PE alignment"); + const auto result = (value + alignment - 1u) & ~std::uint64_t{alignment - 1u}; + if (result > 0xffffffffu) throw std::runtime_error("PE section overflow"); + return static_cast(result); + }; + const auto newHeader = sectionTableOffset + numberOfSections * sizeof(PeSectionHeader); + auto headerLimit = std::size_t{Read(pe, optionalHeaderOffset + 60u)}; + std::uint64_t virtualEnd = headerLimit; + for (std::uint16_t index = 0; index < numberOfSections; ++index) { + const auto section = Read(pe, sectionTableOffset + index * sizeof(PeSectionHeader)); + if (section.PointerToRawData) headerLimit = std::min(headerLimit, std::size_t{section.PointerToRawData}); + virtualEnd = std::max(virtualEnd, std::uint64_t{section.VirtualAddress} + std::max(section.VirtualSize, section.SizeOfRawData)); + } + if (numberOfSections == 0xffffu || newHeader + sizeof(PeSectionHeader) > headerLimit || headerLimit > pe.size()) throw std::runtime_error("no room for PE import section header"); + PeSectionHeader section{}; + const char sectionName[] = ".nidimp"; + std::memcpy(section.Name, sectionName, sizeof(sectionName)); + section.VirtualSize = static_cast(strings.size()); + section.VirtualAddress = align(virtualEnd, sectionAlignment); + section.PointerToRawData = align(pe.size(), fileAlignment); + section.SizeOfRawData = align(strings.size(), fileAlignment); + section.Characteristics = 0x40000040u; + const auto imageSize = align(std::uint64_t{section.VirtualAddress} + section.VirtualSize, sectionAlignment); + const auto rawEnd = std::uint64_t{section.PointerToRawData} + section.SizeOfRawData; + if (rawEnd > 0xffffffffu) throw std::runtime_error("PE file size overflow"); + pe.resize(static_cast(rawEnd), 0); + std::copy(strings.begin(), strings.end(), pe.begin() + section.PointerToRawData); + Write(pe, newHeader, section); + Write(pe, coffHeaderOffset + 2u, static_cast(numberOfSections + 1u)); + Write(pe, optionalHeaderOffset + 56u, imageSize); + Write(pe, optionalHeaderOffset + 64u, std::uint32_t{0}); + for (const auto& item : imports) { + const auto& name = importMap.at(item.Name); + if (name == item.Name) continue; + const auto rva = section.VirtualAddress + stringOffsets.at(name); + for (const auto offset : {item.LookupOffset, item.AddressOffset}) { + if (magic == 0x20bu) Write(pe, offset, std::uint64_t{rva}); + else Write(pe, offset, rva); + } + } + } } diff --git a/core/libs/nid/src/main.cpp b/core/libs/nid/src/main.cpp index 722c765e7c..776480fca6 100644 --- a/core/libs/nid/src/main.cpp +++ b/core/libs/nid/src/main.cpp @@ -28,6 +28,7 @@ void _writeFile(const std::string& path, const std::vector& data) int Run(int argc, char* argv[]) { if (argc < 3) { std::cerr << "Usage: nid_patcher [--preserve-exports | --preserve-export-list ] [file2 ...]\n" + " nid_patcher --exec [--only-sce] [--preserve-exports | --preserve-export-list ] [file2 ...]\n" " nid_patcher --write-export-list \n"; return 1; } @@ -46,14 +47,20 @@ int Run(int argc, char* argv[]) { return 0; } - const std::string libraryName = argv[1]; + Nid::PatchOptions options; + options.executable = std::string(argv[1]) == "--exec"; + const std::string libraryName = options.executable ? "" : argv[1]; std::unordered_set excludedExports; std::vector paths; try { + if (!options.executable && libraryName.starts_with("--")) throw std::runtime_error("unknown option: " + libraryName); bool hasReference = false; for (int i = 2; i < argc; ++i) { const std::string argument = argv[i]; - if (argument == "--preserve-exports" || argument == "--preserve-export-list") { + if (argument == "--only-sce") { + if (!options.executable || options.onlySce) throw std::runtime_error("--only-sce requires --exec and may appear once"); + options.onlySce = true; + } else if (argument == "--preserve-exports" || argument == "--preserve-export-list") { if (hasReference || i + 1 == argc) throw std::runtime_error(argument + " requires exactly one reference"); hasReference = true; excludedExports = argument == "--preserve-exports" ? Nid::ReadExportExclusions(argv[++i]) : Nid::ReadExportList(argv[++i]); @@ -72,7 +79,7 @@ int Run(int argc, char* argv[]) { try { auto binary = _readFile(path); const auto patcher = Nid::MakePatcher(binary); - patcher->PatchNids(binary, libraryName, excludedExports); + patcher->PatchNids(binary, libraryName, excludedExports, options); _writeFile(path, binary); std::cout << "OK: " << path << "\n"; } catch (const std::exception& e) { diff --git a/core/libs/nid/tests/test_macho_nids.py b/core/libs/nid/tests/test_macho_nids.py index 794cbf6d67..0b7e97df74 100644 --- a/core/libs/nid/tests/test_macho_nids.py +++ b/core/libs/nid/tests/test_macho_nids.py @@ -263,7 +263,7 @@ def check_export_list(patcher, directory): def check_refusals(patcher, directory): growing = [("_ab", 0, 0x100), ("_cd", 0, 0x108)] result, _ = patch(patcher, directory, "signed.dylib", build_dylib(growing, [], signed=True)) - assert result.returncode != 0 and "signed Mach-O images cannot grow their export trie" in result.stdout + result.stderr, (result.stdout, result.stderr) + assert result.returncode != 0 and "signed Mach-O images cannot grow linkedit data" in result.stdout + result.stderr, (result.stdout, result.stderr) result, _ = patch(patcher, directory, "opcodes.dylib", build_dylib([("_puts_nid_postfix", 0, 0x100)], [], bind_opcodes=True)) assert result.returncode != 0 and "bind opcodes are not supported" in result.stdout + result.stderr, (result.stdout, result.stderr) result, _ = patch(patcher, directory, "long.dylib", build_dylib([("_puts_nid_postfix", 0, 0x100)], [("_sceXy", 1)])) diff --git a/core/libs/nid/tests/test_pe_references.py b/core/libs/nid/tests/test_pe_references.py index ae26b4c724..0489b2b26c 100644 --- a/core/libs/nid/tests/test_pe_references.py +++ b/core/libs/nid/tests/test_pe_references.py @@ -243,5 +243,178 @@ class ForwardingExportTests(unittest.TestCase): self.assertEqual(after, image) +class ExecutableImportTests(unittest.TestCase): + names = ["foo", "foo_nid_postfix", "z", "sceExample", "helper_nid_no_patch", "plain_nid_no_patch_cut", "SDL_Init", "scePreserved", "hostFunctionLongName", "alias_nid_disambig12_nid_postfix"] + + def pe(self, bits=64): + image = bytearray(2048) + image[:2] = b"MZ" + struct.pack_into(" [file2 ...]` patches imports in place and preserves the application's exports. It detects PE, ELF64 and Mach-O64 from file contents. No library name is required. Build a separate executable for each target platform. + +By default, imports use the library export naming rules: `_nid_no_patch` and `SDL_*` stay unchanged; `_nid_no_patch_cut` is removed without hashing; `_nid_postfix` and numeric `_nid_disambig` suffixes are removed before computing the NID. When both `foo` and `foo_nid_postfix` are imported, `foo` stays unchanged. These rules also apply to host imports unless they are explicitly preserved. + +`nid_patcher --exec --only-sce ` restricts renaming to names starting with `sce` (case-insensitive) or ending with `_nid_postfix`. Both modes accept `--preserve-exports ` or `--preserve-export-list ` for explicit exclusions. The existing `nid_patcher ` mode is unchanged. + +Unsupported layouts fail before the input file is written. See the [format limitations](../dev/debt/libs.md). From ad0d4f5995ef9b9fe26060609f093bcf03c8eced Mon Sep 17 00:00:00 2001 From: David Date: Wed, 21 Oct 2026 20:41:14 +0100 Subject: [PATCH 373/428] feat(libkernel_sys): implement swapcontext --- core/libs/CMakeLists.txt | 12 ++ .../prx/libkernel/System/src/UserContext.cpp | 91 +++++---- .../prx/libkernel_sys/Export.Discovery.cpp | 8 +- core/libs/tests/GuestKernelSysSwapcontext.cpp | 175 ++++++++++++++++++ 4 files changed, 245 insertions(+), 41 deletions(-) create mode 100644 core/libs/tests/GuestKernelSysSwapcontext.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index eea852c88a..f0b4fb7fb2 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -881,6 +881,18 @@ if(BUILD_TESTING) endif() add_test(NAME MemoryPoolTest COMMAND MemoryPoolTest) + add_test_executable(guest_kernel_sys_swapcontext_tests tests/GuestKernelSysSwapcontext.cpp) + target_include_directories(guest_kernel_sys_swapcontext_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_compile_options(guest_kernel_sys_swapcontext_tests PRIVATE -fno-omit-frame-pointer) + target_link_libraries(guest_kernel_sys_swapcontext_tests PRIVATE libkernel_sys libSceLibcInternal libkernel libc) + configure_windows_unwind(guest_kernel_sys_swapcontext_tests) + add_test(NAME guest_kernel_sys_swapcontext COMMAND guest_kernel_sys_swapcontext_tests) + set_tests_properties(guest_kernel_sys_swapcontext PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_kernel_sys_swapcontext PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_acm_tests tests/GuestAcm.cpp) target_include_directories(guest_acm_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_acm_tests PRIVATE libSceAcm libc) diff --git a/core/libs/prx/libkernel/System/src/UserContext.cpp b/core/libs/prx/libkernel/System/src/UserContext.cpp index 0ebbc448f2..10e7e1954b 100644 --- a/core/libs/prx/libkernel/System/src/UserContext.cpp +++ b/core/libs/prx/libkernel/System/src/UserContext.cpp @@ -70,47 +70,58 @@ std::uint32_t HostMxcsrMask() { extern "C" [[noreturn]] void APS5_VABI Aps5ContextRestore_nid_no_patch(const RegisterFrame* frame); +#define APS5_CONTEXT_CAPTURE \ + " pushfq\n" \ + " push %rbp\n" \ + " mov %rsp, %rbp\n" \ + " sub $0x2a0, %rsp\n" \ + " and $-16, %rsp\n" \ + " mov %rax, 0x00(%rsp)\n" \ + " mov %rbx, 0x08(%rsp)\n" \ + " mov %rcx, 0x10(%rsp)\n" \ + " mov %rdx, 0x18(%rsp)\n" \ + " mov %rsi, 0x20(%rsp)\n" \ + " mov %rdi, 0x28(%rsp)\n" \ + " mov 0(%rbp), %rax\n" \ + " mov %rax, 0x30(%rsp)\n" \ + " lea 24(%rbp), %rax\n" \ + " mov %rax, 0x38(%rsp)\n" \ + " mov %r8, 0x40(%rsp)\n" \ + " mov %r9, 0x48(%rsp)\n" \ + " mov %r10, 0x50(%rsp)\n" \ + " mov %r11, 0x58(%rsp)\n" \ + " mov %r12, 0x60(%rsp)\n" \ + " mov %r13, 0x68(%rsp)\n" \ + " mov %r14, 0x70(%rsp)\n" \ + " mov %r15, 0x78(%rsp)\n" \ + " mov 16(%rbp), %rax\n" \ + " mov %rax, 0x80(%rsp)\n" \ + " mov 8(%rbp), %rax\n" \ + " mov %rax, 0x88(%rsp)\n" \ + " xor %eax, %eax\n" \ + " mov %cs, %ax\n" \ + " mov %rax, 0x90(%rsp)\n" \ + " mov %ss, %ax\n" \ + " mov %rax, 0x98(%rsp)\n" \ + " fxsave64 0xa0(%rsp)\n" + +#define APS5_CONTEXT_RETURN \ + " mov %rbp, %rsp\n" \ + " pop %rbp\n" \ + " lea 8(%rsp), %rsp\n" \ + " ret\n" + asm(".text\n" APS5_ASM_FUNCTION("getcontext_nid_postfix") - " pushfq\n" - " push %rbp\n" - " mov %rsp, %rbp\n" - " sub $0x2a0, %rsp\n" - " and $-16, %rsp\n" - " mov %rax, 0x00(%rsp)\n" - " mov %rbx, 0x08(%rsp)\n" - " mov %rcx, 0x10(%rsp)\n" - " mov %rdx, 0x18(%rsp)\n" - " mov %rsi, 0x20(%rsp)\n" - " mov %rdi, 0x28(%rsp)\n" - " mov 0(%rbp), %rax\n" - " mov %rax, 0x30(%rsp)\n" - " lea 24(%rbp), %rax\n" - " mov %rax, 0x38(%rsp)\n" - " mov %r8, 0x40(%rsp)\n" - " mov %r9, 0x48(%rsp)\n" - " mov %r10, 0x50(%rsp)\n" - " mov %r11, 0x58(%rsp)\n" - " mov %r12, 0x60(%rsp)\n" - " mov %r13, 0x68(%rsp)\n" - " mov %r14, 0x70(%rsp)\n" - " mov %r15, 0x78(%rsp)\n" - " mov 16(%rbp), %rax\n" - " mov %rax, 0x80(%rsp)\n" - " mov 8(%rbp), %rax\n" - " mov %rax, 0x88(%rsp)\n" - " xor %eax, %eax\n" - " mov %cs, %ax\n" - " mov %rax, 0x90(%rsp)\n" - " mov %ss, %ax\n" - " mov %rax, 0x98(%rsp)\n" - " fxsave64 0xa0(%rsp)\n" + APS5_CONTEXT_CAPTURE " mov %rsp, %rsi\n" " call " APS5_ASM_SYMBOL("Aps5GetcontextFinish_nid_no_patch") "\n" - " mov %rbp, %rsp\n" - " pop %rbp\n" - " lea 8(%rsp), %rsp\n" - " ret\n" + APS5_CONTEXT_RETURN + APS5_ASM_FUNCTION("Aps5Swapcontext_nid_no_patch") + APS5_CONTEXT_CAPTURE + " mov %rsp, %rdx\n" + " call " APS5_ASM_SYMBOL("Aps5SwapcontextFinish_nid_no_patch") "\n" + APS5_CONTEXT_RETURN APS5_ASM_FUNCTION("Aps5ContextRestore_nid_no_patch") " mov %rdi, %r12\n" " xor %eax, %eax\n" @@ -217,4 +228,10 @@ int APS5_VABI setcontext_nid_postfix(const UserUcontext* context) { Aps5ContextRestore_nid_no_patch(&frame); } +int APS5_VABI Aps5SwapcontextFinish_nid_no_patch(UserUcontext* saved, const UserUcontext* next, const RegisterFrame* frame) { + if (saved == nullptr || next == nullptr) return Fail(GuestEinval); + Aps5GetcontextFinish_nid_no_patch(saved, frame); + return setcontext_nid_postfix(next); +} + } diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 3addcda651..f405bb2f62 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -3,6 +3,7 @@ #include "prx/libkernel/include/Api.hpp" #include "prx/libc/include/Api.hpp" #include "SceTypes.hpp" +#include "prx/libc/include/general/AsmMacros.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/KernelErrors.hpp" #include @@ -2340,10 +2341,9 @@ int APS5_VABI statfs_nid_postfix(void) { return 0; } -int APS5_VABI swapcontext_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} +asm(".text\n" + APS5_ASM_FUNCTION("swapcontext_nid_postfix") + " jmp " APS5_ASM_SYMBOL("Aps5Swapcontext_nid_no_patch") "\n"); int APS5_VABI swapon_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); diff --git a/core/libs/tests/GuestKernelSysSwapcontext.cpp b/core/libs/tests/GuestKernelSysSwapcontext.cpp new file mode 100644 index 0000000000..3993a6b26c --- /dev/null +++ b/core/libs/tests/GuestKernelSysSwapcontext.cpp @@ -0,0 +1,175 @@ +#include "prx/libc/include/general/AsmMacros.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/System/include/UserContext.hpp" +#include +#include +#include +#include +#include + +using ContextEntry = void (APS5_VABI*)(); + +extern "C" { +[[gnu::returns_twice]] int APS5_VABI getcontext_nid_postfix(UserUcontext* context); +[[gnu::returns_twice]] int APS5_VABI swapcontext_nid_postfix(UserUcontext* saved, const UserUcontext* next); +void APS5_VABI makecontext_nid_postfix(UserUcontext* context, ContextEntry entry, int count, ...); +int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSet); +int* APS5_VABI __error_nid_postfix(); + +int APS5_VABI SwapKeepingRegisters(UserUcontext* saved, const UserUcontext* next); +int APS5_VABI SwapClobberingRegisters(UserUcontext* saved, const UserUcontext* next); +} + +asm(".text\n" + APS5_ASM_FUNCTION("SwapKeepingRegisters") + " push %rbx\n" + " push %rbp\n" + " push %r12\n" + " push %r13\n" + " push %r14\n" + " push %r15\n" + " sub $8, %rsp\n" + " movabsq $0x1111111111111111, %rbx\n" + " movabsq $0x2222222222222222, %rbp\n" + " movabsq $0x3333333333333333, %r12\n" + " movabsq $0x4444444444444444, %r13\n" + " movabsq $0x5555555555555555, %r14\n" + " movabsq $0x6666666666666666, %r15\n" + " call " APS5_ASM_SYMBOL("swapcontext_nid_postfix") "\n" + " movabsq $0x1111111111111111, %rcx\n" + " cmp %rcx, %rbx\n" + " jne 1f\n" + " movabsq $0x2222222222222222, %rcx\n" + " cmp %rcx, %rbp\n" + " jne 1f\n" + " movabsq $0x3333333333333333, %rcx\n" + " cmp %rcx, %r12\n" + " jne 1f\n" + " movabsq $0x4444444444444444, %rcx\n" + " cmp %rcx, %r13\n" + " jne 1f\n" + " movabsq $0x5555555555555555, %rcx\n" + " cmp %rcx, %r14\n" + " jne 1f\n" + " movabsq $0x6666666666666666, %rcx\n" + " cmp %rcx, %r15\n" + " je 2f\n" + "1:\n" + " mov $-100, %eax\n" + "2:\n" + " add $8, %rsp\n" + " pop %r15\n" + " pop %r14\n" + " pop %r13\n" + " pop %r12\n" + " pop %rbp\n" + " pop %rbx\n" + " ret\n" + APS5_ASM_FUNCTION("SwapClobberingRegisters") + " push %rbx\n" + " push %rbp\n" + " push %r12\n" + " push %r13\n" + " push %r14\n" + " push %r15\n" + " sub $8, %rsp\n" + " movabsq $0x0badc0de0badc0de, %rbx\n" + " mov %rbx, %rbp\n" + " mov %rbx, %r12\n" + " mov %rbx, %r13\n" + " mov %rbx, %r14\n" + " mov %rbx, %r15\n" + " call " APS5_ASM_SYMBOL("swapcontext_nid_postfix") "\n" + " add $8, %rsp\n" + " pop %r15\n" + " pop %r14\n" + " pop %r13\n" + " pop %r12\n" + " pop %rbp\n" + " pop %rbx\n" + " ret\n"); + +namespace { + +constexpr int Einval = 22; +constexpr int SigBlock = 1; +constexpr int SigSetmask = 3; +constexpr int SigUsr1 = 30; +constexpr std::size_t StackBytes = 256 * 1024; + +struct alignas(16) Stack { + unsigned char bytes[StackBytes]; +}; + +Stack g_stack; +UserUcontext g_main = {}; +UserUcontext g_coroutine = {}; +int g_steps = 0; +bool g_coroutineSawBlocked = true; + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "check failed at line %u\n", static_cast(location.line())); + std::exit(1); +} + +int Error() { + return *__error_nid_postfix(); +} + +bool Usr1Blocked() { + std::uint32_t mask[4] = {}; + Require(sigprocmask_nid_postfix(SigBlock, nullptr, mask) == 0); + return (mask[0] & (1u << (SigUsr1 - 1))) != 0; +} + +void APS5_VABI Coroutine() { + g_coroutineSawBlocked = Usr1Blocked(); + for (int step = 0; step < 3; ++step) { + ++g_steps; + Require(SwapClobberingRegisters(&g_coroutine, &g_main) == 0); + } +} + +void Arguments() { + UserUcontext context = {}; + Require(getcontext_nid_postfix(&context) == 0); + Require(swapcontext_nid_postfix(nullptr, &context) == -1 && Error() == Einval); + Require(swapcontext_nid_postfix(&context, nullptr) == -1 && Error() == Einval); + + UserUcontext saved = {}; + UserUcontext invalid = context; + invalid.mcontext.len = 0; + Require(swapcontext_nid_postfix(&saved, &invalid) == -1 && Error() == Einval); + Require(saved.mcontext.len == sizeof(UserMcontext) && saved.mcontext.rip != 0); +} + +void PingPong() { + Require(getcontext_nid_postfix(&g_coroutine) == 0); + g_coroutine.stackPointer = g_stack.bytes; + g_coroutine.stackSize = sizeof(g_stack.bytes); + g_coroutine.link = &g_main; + makecontext_nid_postfix(&g_coroutine, &Coroutine, 0); + Require(g_coroutine.mcontext.len == sizeof(UserMcontext)); + + std::uint32_t block[4] = {1u << (SigUsr1 - 1), 0, 0, 0}; + std::uint32_t previous[4] = {}; + Require(sigprocmask_nid_postfix(SigBlock, block, previous) == 0); + for (int expected = 1; expected <= 3; ++expected) { + Require(SwapKeepingRegisters(&g_main, &g_coroutine) == 0); + Require(g_steps == expected && Usr1Blocked()); + } + Require(!g_coroutineSawBlocked); + Require(SwapKeepingRegisters(&g_main, &g_coroutine) == 0); + Require(g_steps == 3 && Usr1Blocked()); + Require(sigprocmask_nid_postfix(SigSetmask, previous, nullptr) == 0); +} + +} + +int main() { + Arguments(); + PingPong(); + std::puts("guest_kernel_sys_swapcontext passed"); + return 0; +} From 5881575782ee1de27cfbdc90ec3e42ecf32d0df5 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 21 Oct 2026 22:35:16 +0100 Subject: [PATCH 374/428] refactor(libkernel): export swapcontext from libkernel directly The PS5 3.20 export lists have swapcontext in libkernel as well as libkernel_sys, and on Windows a libkernel import never reaches libkernel_sys. The assembly entry in UserContext.cpp now is swapcontext_nid_postfix, and the libkernel_sys jump stub is gone. --- core/libs/prx/libkernel/System/src/UserContext.cpp | 2 +- core/libs/prx/libkernel_sys/Export.Discovery.cpp | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/UserContext.cpp b/core/libs/prx/libkernel/System/src/UserContext.cpp index 10e7e1954b..0d3d7f5937 100644 --- a/core/libs/prx/libkernel/System/src/UserContext.cpp +++ b/core/libs/prx/libkernel/System/src/UserContext.cpp @@ -117,7 +117,7 @@ asm(".text\n" " mov %rsp, %rsi\n" " call " APS5_ASM_SYMBOL("Aps5GetcontextFinish_nid_no_patch") "\n" APS5_CONTEXT_RETURN - APS5_ASM_FUNCTION("Aps5Swapcontext_nid_no_patch") + APS5_ASM_FUNCTION("swapcontext_nid_postfix") APS5_CONTEXT_CAPTURE " mov %rsp, %rdx\n" " call " APS5_ASM_SYMBOL("Aps5SwapcontextFinish_nid_no_patch") "\n" diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index f405bb2f62..8ad8f17262 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -3,7 +3,6 @@ #include "prx/libkernel/include/Api.hpp" #include "prx/libc/include/Api.hpp" #include "SceTypes.hpp" -#include "prx/libc/include/general/AsmMacros.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/KernelErrors.hpp" #include @@ -2341,10 +2340,6 @@ int APS5_VABI statfs_nid_postfix(void) { return 0; } -asm(".text\n" - APS5_ASM_FUNCTION("swapcontext_nid_postfix") - " jmp " APS5_ASM_SYMBOL("Aps5Swapcontext_nid_no_patch") "\n"); - int APS5_VABI swapon_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; From 3bf3f7928bd0dc3a19049d887afe4e881574fb7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 25 Oct 2026 13:04:17 +0100 Subject: [PATCH 375/428] fix(libkernel): declare swapcontext in libkernel's Api.hpp Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/include/Api.hpp | 1 + core/libs/prx/libkernel_sys/include/Api.hpp | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/include/Api.hpp b/core/libs/prx/libkernel/include/Api.hpp index 35b536672a..08bf7cab4f 100644 --- a/core/libs/prx/libkernel/include/Api.hpp +++ b/core/libs/prx/libkernel/include/Api.hpp @@ -775,6 +775,7 @@ unsigned int APS5_VABI sceKernelSleep(unsigned int seconds); [[noreturn]] void APS5_VABI _exit_nid_postfix(int status); [[gnu::returns_twice]] int APS5_VABI getcontext_nid_postfix(UserUcontext* context); +[[gnu::returns_twice]] int APS5_VABI swapcontext_nid_postfix(UserUcontext* oucp, const UserUcontext* ucp); [[gnu::returns_twice]] int APS5_VABI sigsetjmp_nid_postfix(std::uint64_t* buffer, int saveMask); [[noreturn]] void APS5_VABI siglongjmp_nid_postfix(std::uint64_t* buffer, int value); diff --git a/core/libs/prx/libkernel_sys/include/Api.hpp b/core/libs/prx/libkernel_sys/include/Api.hpp index 938bc8178b..61a33fb6d6 100644 --- a/core/libs/prx/libkernel_sys/include/Api.hpp +++ b/core/libs/prx/libkernel_sys/include/Api.hpp @@ -538,7 +538,6 @@ int APS5_VABI settimeofday_nid_postfix(void); int APS5_VABI sigqueue_nid_postfix(void); int APS5_VABI sigreturn_nid_postfix(void); int APS5_VABI statfs_nid_postfix(void); -int APS5_VABI swapcontext_nid_postfix(void); int APS5_VABI swapon_nid_postfix(void); int APS5_VABI symlinkat_nid_postfix(void); int APS5_VABI sysKernelGetIntdevModeForInternalLibc_nid_postfix(void); From e3db6dacf752617b8f6b2fd955ecb6a426a45b80 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sun, 25 Oct 2026 15:05:11 +0300 Subject: [PATCH 376/428] docs(nid): remove executable patching debt entry --- docs/dev/debt/libs.md | 2 -- docs/user/USAGE.md | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/dev/debt/libs.md b/docs/dev/debt/libs.md index 34010cabd7..1e07f7ef14 100644 --- a/docs/dev/debt/libs.md +++ b/docs/dev/debt/libs.md @@ -7,5 +7,3 @@ ### Functional - [GuestKernelSys.cpp](../../../core/libs/tests/GuestKernelSys.cpp) (libs) - Windows removes the fixture's symbolic links and the fixture without std::filesystem: the MinGW libstdc++ (GCC 15.2.0) reports a dangling symbolic link as missing and clears the error, so `remove` neither deletes it nor fails, and `remove_all` loops forever over a tree that holds one (`recursive_directory_iterator::__erase` retries the entry while the error stays clear). The tests delete reparse points with `DeleteFileW` and `RemoveDirectoryW` and the fixture through the shell - -- [nid_patcher](../../../core/libs/nid/src/main.cpp) (libs) - `--exec` supports little-endian ELF64 with section headers, PE32/PE32+ named imports and thin Mach-O64 chained imports. ELF symbol version definitions, Mach-O bind opcodes and compressed chained symbols are rejected. Signed Mach-O inputs must have their signature removed before patching and be signed again afterwards. Signed PE, bound imports and delay imports are rejected; a growing PE import table requires space for another section header. ELF/Mach-O loader execution of this mode has not been validated on their native platforms. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index bc6aa08113..8ae0c46dc3 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -452,4 +452,4 @@ By default, imports use the library export naming rules: `_nid_no_patch` and `SD `nid_patcher --exec --only-sce ` restricts renaming to names starting with `sce` (case-insensitive) or ending with `_nid_postfix`. Both modes accept `--preserve-exports ` or `--preserve-export-list ` for explicit exclusions. The existing `nid_patcher ` mode is unchanged. -Unsupported layouts fail before the input file is written. See the [format limitations](../dev/debt/libs.md). +Unsupported layouts fail before the input file is written. From e3ccd46f2690b53f3eaa599bc8024702fea174d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 25 Oct 2026 13:14:04 +0100 Subject: [PATCH 377/428] test(libkernel): use the public Api headers in the swapcontext test Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestKernelSysSwapcontext.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/core/libs/tests/GuestKernelSysSwapcontext.cpp b/core/libs/tests/GuestKernelSysSwapcontext.cpp index 3993a6b26c..1aa364e87e 100644 --- a/core/libs/tests/GuestKernelSysSwapcontext.cpp +++ b/core/libs/tests/GuestKernelSysSwapcontext.cpp @@ -1,3 +1,6 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libSceLibcInternal/include/Api.hpp" #include "prx/libc/include/general/AsmMacros.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/System/include/UserContext.hpp" @@ -10,12 +13,6 @@ using ContextEntry = void (APS5_VABI*)(); extern "C" { -[[gnu::returns_twice]] int APS5_VABI getcontext_nid_postfix(UserUcontext* context); -[[gnu::returns_twice]] int APS5_VABI swapcontext_nid_postfix(UserUcontext* saved, const UserUcontext* next); -void APS5_VABI makecontext_nid_postfix(UserUcontext* context, ContextEntry entry, int count, ...); -int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSet); -int* APS5_VABI __error_nid_postfix(); - int APS5_VABI SwapKeepingRegisters(UserUcontext* saved, const UserUcontext* next); int APS5_VABI SwapClobberingRegisters(UserUcontext* saved, const UserUcontext* next); } From 22e5cd7e1ac01b3282a4e2506e52ae8829d918a5 Mon Sep 17 00:00:00 2001 From: Rookie <175737347+rooklz@users.noreply.github.com> Date: Sat, 24 Oct 2026 20:17:51 +0100 Subject: [PATCH 378/428] fix(libSceCesCs): decode KS X 1001 cell 0xA2E8 as U+327E as the console does --- core/libs/prx/libSceCesCs/Cp949.hpp | 2 +- core/libs/tests/GuestCesDbcs.cpp | 6 ++++-- docs/dev/debt/libSceCesCs.md | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceCesCs/Cp949.hpp b/core/libs/prx/libSceCesCs/Cp949.hpp index ec0166ce13..54c8333dd4 100644 --- a/core/libs/prx/libSceCesCs/Cp949.hpp +++ b/core/libs/prx/libSceCesCs/Cp949.hpp @@ -38,7 +38,7 @@ inline constexpr uint16_t TO_UCS2[126 * 190] = { 0xC6A8, 0xC6AA, 0xC6AB, 0xC6AC, 0xC6AD, 0xC6AE, 0xC6AF, 0xC6B2, 0xC6B3, 0xC6B5, 0xC6B6, 0xC6B7, 0xC6BB, 0xC6BC, 0xC6BD, 0xC6BE, 0xC6BF, 0xC6C2, 0xC6C4, 0xC6C6, 0xC6C7, 0xC6C8, 0xC6C9, 0xC6CA, 0xC6CB, 0xC6CE, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC6CF, 0xC6D1, 0xC6D2, 0xC6D3, 0xC6D5, 0xC6D6, 0xC6D7, 0xC6D8, 0xC6D9, 0xC6DA, 0xC6DB, 0xC6DE, 0xC6DF, 0xC6E2, 0xC6E3, 0xC6E4, 0xC6E5, 0xC6E6, 0xC6E7, 0xC6EA, 0xC6EB, 0xC6ED, 0xC6EE, 0xC6EF, 0xC6F1, 0xC6F2, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC6F3, 0xC6F4, 0xC6F5, 0xC6F6, 0xC6F7, 0xC6FA, 0xC6FB, 0xC6FC, 0xC6FE, 0xC6FF, 0xC700, 0xC701, 0xC702, 0xC703, 0xC706, 0xC707, 0xC709, 0xC70A, 0xC70B, 0xC70D, 0xC70E, 0xC70F, 0xC710, 0xC711, 0xC712, 0xC713, 0xC716, 0xC718, 0xC71A, 0xC71B, 0xC71C, 0xC71D, 0xC71E, 0xC71F, 0xC722, 0xC723, 0xC725, 0xC726, 0xC727, 0xC729, 0xC72A, 0xC72B, 0xC72C, 0xC72D, 0xC72E, 0xC72F, 0xC732, 0xC734, 0xC736, 0xC738, 0xC739, 0xC73A, 0xC73B, 0xC73E, 0xC73F, 0xC741, 0xC742, 0xC743, 0xC745, 0xC746, 0xC747, 0xC748, 0xC749, 0xC74B, 0xC74E, 0xC750, 0xC759, 0xC75A, 0xC75B, 0xC75D, 0xC75E, 0xC75F, 0xC761, 0xC762, 0xC763, 0xC764, 0xC765, 0xC766, 0xC767, 0xC769, 0xC76A, 0xC76C, 0xC76D, 0xC76E, 0xC76F, 0xC770, 0xC771, 0xC772, 0xC773, 0xC776, 0xC777, 0xC779, 0xC77A, 0xC77B, 0xC77F, 0xC780, 0xC781, 0xC782, 0xC786, 0xC78B, 0xC78C, 0xC78D, 0xC78F, 0xC792, 0xC793, 0xC795, 0xC799, 0xC79B, 0xC79C, 0xC79D, 0xC79E, 0xC79F, 0xC7A2, 0xC7A7, 0xC7A8, 0xC7A9, 0xC7AA, 0xC7AB, 0xC7AE, 0xC7AF, 0xC7B1, 0xC7B2, 0xC7B3, 0xC7B5, 0xC7B6, 0xC7B7, 0xC7B8, 0xC7B9, 0xC7BA, 0xC7BB, 0xC7BE, 0xC7C2, 0xC7C3, 0xC7C4, 0xC7C5, 0xC7C6, 0xC7C7, 0xC7CA, 0xC7CB, 0xC7CD, 0xC7CF, 0xC7D1, 0xC7D2, 0xC7D3, 0xC7D4, 0xC7D5, 0xC7D6, 0xC7D7, 0xC7D9, 0xC7DA, 0xC7DB, 0xC7DC, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC7DE, 0xC7DF, 0xC7E0, 0xC7E1, 0xC7E2, 0xC7E3, 0xC7E5, 0xC7E6, 0xC7E7, 0xC7E9, 0xC7EA, 0xC7EB, 0xC7ED, 0xC7EE, 0xC7EF, 0xC7F0, 0xC7F1, 0xC7F2, 0xC7F3, 0xC7F4, 0xC7F5, 0xC7F6, 0xC7F7, 0xC7F8, 0xC7F9, 0xC7FA, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC7FB, 0xC7FC, 0xC7FD, 0xC7FE, 0xC7FF, 0xC802, 0xC803, 0xC805, 0xC806, 0xC807, 0xC809, 0xC80B, 0xC80C, 0xC80D, 0xC80E, 0xC80F, 0xC812, 0xC814, 0xC817, 0xC818, 0xC819, 0xC81A, 0xC81B, 0xC81E, 0xC81F, 0xC821, 0xC822, 0xC823, 0xC825, 0xC826, 0xC827, 0xC828, 0xC829, 0xC82A, 0xC82B, 0xC82E, 0xC830, 0xC832, 0xC833, 0xC834, 0xC835, 0xC836, 0xC837, 0xC839, 0xC83A, 0xC83B, 0xC83D, 0xC83E, 0xC83F, 0xC841, 0xC842, 0xC843, 0xC844, 0xC845, 0xC846, 0xC847, 0xC84A, 0xC84B, 0xC84E, 0xC84F, 0xC850, 0xC851, 0xC852, 0xC853, 0xC855, 0xC856, 0xC857, 0xC858, 0xC859, 0xC85A, 0xC85B, 0xC85C, 0xC85D, 0xC85E, 0xC85F, 0xC860, 0xC861, 0xC862, 0xC863, 0xC864, 0xC865, 0xC866, 0xC867, 0xC868, 0xC869, 0xC86A, 0xC86B, 0xC86C, 0xC86D, 0xC86E, 0xC86F, 0xC872, 0xC873, 0xC875, 0xC876, 0xC877, 0xC879, 0xC87B, 0xC87C, 0xC87D, 0xC87E, 0xC87F, 0xC882, 0xC884, 0xC888, 0xC889, 0xC88A, 0xC88E, 0xC88F, 0xC890, 0xC891, 0xC892, 0xC893, 0xC895, 0xC896, 0xC897, 0xC898, 0xC899, 0xC89A, 0xC89B, 0xC89C, 0xC89E, 0xC8A0, 0xC8A2, 0xC8A3, 0xC8A4, 0xC8A5, 0xC8A6, 0xC8A7, 0xC8A9, 0xC8AA, 0xC8AB, 0xC8AC, 0xC8AD, 0xC8AE, 0xC8AF, 0xC8B0, 0xC8B1, 0xC8B2, 0xC8B3, 0xC8B4, 0xC8B5, 0xC8B6, 0xC8B7, 0xC8B8, 0xC8B9, 0xC8BA, 0xC8BB, 0xC8BE, 0xC8BF, 0xC8C0, 0xC8C1, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC8C2, 0xC8C3, 0xC8C5, 0xC8C6, 0xC8C7, 0xC8C9, 0xC8CA, 0xC8CB, 0xC8CD, 0xC8CE, 0xC8CF, 0xC8D0, 0xC8D1, 0xC8D2, 0xC8D3, 0xC8D6, 0xC8D8, 0xC8DA, 0xC8DB, 0xC8DC, 0xC8DD, 0xC8DE, 0xC8DF, 0xC8E2, 0xC8E3, 0xC8E5, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC8E6, 0xC8E7, 0xC8E8, 0xC8E9, 0xC8EA, 0xC8EB, 0xC8EC, 0xC8ED, 0xC8EE, 0xC8EF, 0xC8F0, 0xC8F1, 0xC8F2, 0xC8F3, 0xC8F4, 0xC8F6, 0xC8F7, 0xC8F8, 0xC8F9, 0xC8FA, 0xC8FB, 0xC8FE, 0xC8FF, 0xC901, 0xC902, 0xC903, 0xC907, 0xC908, 0xC909, 0xC90A, 0xC90B, 0xC90E, 0x3000, 0x3001, 0x3002, 0x00B7, 0x2025, 0x2026, 0x00A8, 0x3003, 0x00AD, 0x2015, 0x2225, 0xFF3C, 0x223C, 0x2018, 0x2019, 0x201C, 0x201D, 0x3014, 0x3015, 0x3008, 0x3009, 0x300A, 0x300B, 0x300C, 0x300D, 0x300E, 0x300F, 0x3010, 0x3011, 0x00B1, 0x00D7, 0x00F7, 0x2260, 0x2264, 0x2265, 0x221E, 0x2234, 0x00B0, 0x2032, 0x2033, 0x2103, 0x212B, 0xFFE0, 0xFFE1, 0xFFE5, 0x2642, 0x2640, 0x2220, 0x22A5, 0x2312, 0x2202, 0x2207, 0x2261, 0x2252, 0x00A7, 0x203B, 0x2606, 0x2605, 0x25CB, 0x25CF, 0x25CE, 0x25C7, 0x25C6, 0x25A1, 0x25A0, 0x25B3, 0x25B2, 0x25BD, 0x25BC, 0x2192, 0x2190, 0x2191, 0x2193, 0x2194, 0x3013, 0x226A, 0x226B, 0x221A, 0x223D, 0x221D, 0x2235, 0x222B, 0x222C, 0x2208, 0x220B, 0x2286, 0x2287, 0x2282, 0x2283, 0x222A, 0x2229, 0x2227, 0x2228, 0xFFE2, - 0xC910, 0xC912, 0xC913, 0xC914, 0xC915, 0xC916, 0xC917, 0xC919, 0xC91A, 0xC91B, 0xC91C, 0xC91D, 0xC91E, 0xC91F, 0xC920, 0xC921, 0xC922, 0xC923, 0xC924, 0xC925, 0xC926, 0xC927, 0xC928, 0xC929, 0xC92A, 0xC92B, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC92D, 0xC92E, 0xC92F, 0xC930, 0xC931, 0xC932, 0xC933, 0xC935, 0xC936, 0xC937, 0xC938, 0xC939, 0xC93A, 0xC93B, 0xC93C, 0xC93D, 0xC93E, 0xC93F, 0xC940, 0xC941, 0xC942, 0xC943, 0xC944, 0xC945, 0xC946, 0xC947, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC948, 0xC949, 0xC94A, 0xC94B, 0xC94C, 0xC94D, 0xC94E, 0xC94F, 0xC952, 0xC953, 0xC955, 0xC956, 0xC957, 0xC959, 0xC95A, 0xC95B, 0xC95C, 0xC95D, 0xC95E, 0xC95F, 0xC962, 0xC964, 0xC965, 0xC966, 0xC967, 0xC968, 0xC969, 0xC96A, 0xC96B, 0xC96D, 0xC96E, 0xC96F, 0x21D2, 0x21D4, 0x2200, 0x2203, 0x00B4, 0xFF5E, 0x02C7, 0x02D8, 0x02DD, 0x02DA, 0x02D9, 0x00B8, 0x02DB, 0x00A1, 0x00BF, 0x02D0, 0x222E, 0x2211, 0x220F, 0x00A4, 0x2109, 0x2030, 0x25C1, 0x25C0, 0x25B7, 0x25B6, 0x2664, 0x2660, 0x2661, 0x2665, 0x2667, 0x2663, 0x2299, 0x25C8, 0x25A3, 0x25D0, 0x25D1, 0x2592, 0x25A4, 0x25A5, 0x25A8, 0x25A7, 0x25A6, 0x25A9, 0x2668, 0x260F, 0x260E, 0x261C, 0x261E, 0x00B6, 0x2020, 0x2021, 0x2195, 0x2197, 0x2199, 0x2196, 0x2198, 0x266D, 0x2669, 0x266A, 0x266C, 0x327F, 0x321C, 0x2116, 0x33C7, 0x2122, 0x33C2, 0x33D8, 0x2121, 0x20AC, 0x00AE, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0xC910, 0xC912, 0xC913, 0xC914, 0xC915, 0xC916, 0xC917, 0xC919, 0xC91A, 0xC91B, 0xC91C, 0xC91D, 0xC91E, 0xC91F, 0xC920, 0xC921, 0xC922, 0xC923, 0xC924, 0xC925, 0xC926, 0xC927, 0xC928, 0xC929, 0xC92A, 0xC92B, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC92D, 0xC92E, 0xC92F, 0xC930, 0xC931, 0xC932, 0xC933, 0xC935, 0xC936, 0xC937, 0xC938, 0xC939, 0xC93A, 0xC93B, 0xC93C, 0xC93D, 0xC93E, 0xC93F, 0xC940, 0xC941, 0xC942, 0xC943, 0xC944, 0xC945, 0xC946, 0xC947, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC948, 0xC949, 0xC94A, 0xC94B, 0xC94C, 0xC94D, 0xC94E, 0xC94F, 0xC952, 0xC953, 0xC955, 0xC956, 0xC957, 0xC959, 0xC95A, 0xC95B, 0xC95C, 0xC95D, 0xC95E, 0xC95F, 0xC962, 0xC964, 0xC965, 0xC966, 0xC967, 0xC968, 0xC969, 0xC96A, 0xC96B, 0xC96D, 0xC96E, 0xC96F, 0x21D2, 0x21D4, 0x2200, 0x2203, 0x00B4, 0xFF5E, 0x02C7, 0x02D8, 0x02DD, 0x02DA, 0x02D9, 0x00B8, 0x02DB, 0x00A1, 0x00BF, 0x02D0, 0x222E, 0x2211, 0x220F, 0x00A4, 0x2109, 0x2030, 0x25C1, 0x25C0, 0x25B7, 0x25B6, 0x2664, 0x2660, 0x2661, 0x2665, 0x2667, 0x2663, 0x2299, 0x25C8, 0x25A3, 0x25D0, 0x25D1, 0x2592, 0x25A4, 0x25A5, 0x25A8, 0x25A7, 0x25A6, 0x25A9, 0x2668, 0x260F, 0x260E, 0x261C, 0x261E, 0x00B6, 0x2020, 0x2021, 0x2195, 0x2197, 0x2199, 0x2196, 0x2198, 0x266D, 0x2669, 0x266A, 0x266C, 0x327F, 0x321C, 0x2116, 0x33C7, 0x2122, 0x33C2, 0x33D8, 0x2121, 0x20AC, 0x00AE, 0x327E, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC971, 0xC972, 0xC973, 0xC975, 0xC976, 0xC977, 0xC978, 0xC979, 0xC97A, 0xC97B, 0xC97D, 0xC97E, 0xC97F, 0xC980, 0xC981, 0xC982, 0xC983, 0xC984, 0xC985, 0xC986, 0xC987, 0xC98A, 0xC98B, 0xC98D, 0xC98E, 0xC98F, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC991, 0xC992, 0xC993, 0xC994, 0xC995, 0xC996, 0xC997, 0xC99A, 0xC99C, 0xC99E, 0xC99F, 0xC9A0, 0xC9A1, 0xC9A2, 0xC9A3, 0xC9A4, 0xC9A5, 0xC9A6, 0xC9A7, 0xC9A8, 0xC9A9, 0xC9AA, 0xC9AB, 0xC9AC, 0xC9AD, 0xC9AE, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC9AF, 0xC9B0, 0xC9B1, 0xC9B2, 0xC9B3, 0xC9B4, 0xC9B5, 0xC9B6, 0xC9B7, 0xC9B8, 0xC9B9, 0xC9BA, 0xC9BB, 0xC9BC, 0xC9BD, 0xC9BE, 0xC9BF, 0xC9C2, 0xC9C3, 0xC9C5, 0xC9C6, 0xC9C9, 0xC9CB, 0xC9CC, 0xC9CD, 0xC9CE, 0xC9CF, 0xC9D2, 0xC9D4, 0xC9D7, 0xC9D8, 0xC9DB, 0xFF01, 0xFF02, 0xFF03, 0xFF04, 0xFF05, 0xFF06, 0xFF07, 0xFF08, 0xFF09, 0xFF0A, 0xFF0B, 0xFF0C, 0xFF0D, 0xFF0E, 0xFF0F, 0xFF10, 0xFF11, 0xFF12, 0xFF13, 0xFF14, 0xFF15, 0xFF16, 0xFF17, 0xFF18, 0xFF19, 0xFF1A, 0xFF1B, 0xFF1C, 0xFF1D, 0xFF1E, 0xFF1F, 0xFF20, 0xFF21, 0xFF22, 0xFF23, 0xFF24, 0xFF25, 0xFF26, 0xFF27, 0xFF28, 0xFF29, 0xFF2A, 0xFF2B, 0xFF2C, 0xFF2D, 0xFF2E, 0xFF2F, 0xFF30, 0xFF31, 0xFF32, 0xFF33, 0xFF34, 0xFF35, 0xFF36, 0xFF37, 0xFF38, 0xFF39, 0xFF3A, 0xFF3B, 0xFFE6, 0xFF3D, 0xFF3E, 0xFF3F, 0xFF40, 0xFF41, 0xFF42, 0xFF43, 0xFF44, 0xFF45, 0xFF46, 0xFF47, 0xFF48, 0xFF49, 0xFF4A, 0xFF4B, 0xFF4C, 0xFF4D, 0xFF4E, 0xFF4F, 0xFF50, 0xFF51, 0xFF52, 0xFF53, 0xFF54, 0xFF55, 0xFF56, 0xFF57, 0xFF58, 0xFF59, 0xFF5A, 0xFF5B, 0xFF5C, 0xFF5D, 0xFFE3, 0xC9DE, 0xC9DF, 0xC9E1, 0xC9E3, 0xC9E5, 0xC9E6, 0xC9E8, 0xC9E9, 0xC9EA, 0xC9EB, 0xC9EE, 0xC9F2, 0xC9F3, 0xC9F4, 0xC9F5, 0xC9F6, 0xC9F7, 0xC9FA, 0xC9FB, 0xC9FD, 0xC9FE, 0xC9FF, 0xCA01, 0xCA02, 0xCA03, 0xCA04, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCA05, 0xCA06, 0xCA07, 0xCA0A, 0xCA0E, 0xCA0F, 0xCA10, 0xCA11, 0xCA12, 0xCA13, 0xCA15, 0xCA16, 0xCA17, 0xCA19, 0xCA1A, 0xCA1B, 0xCA1C, 0xCA1D, 0xCA1E, 0xCA1F, 0xCA20, 0xCA21, 0xCA22, 0xCA23, 0xCA24, 0xCA25, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCA26, 0xCA27, 0xCA28, 0xCA2A, 0xCA2B, 0xCA2C, 0xCA2D, 0xCA2E, 0xCA2F, 0xCA30, 0xCA31, 0xCA32, 0xCA33, 0xCA34, 0xCA35, 0xCA36, 0xCA37, 0xCA38, 0xCA39, 0xCA3A, 0xCA3B, 0xCA3C, 0xCA3D, 0xCA3E, 0xCA3F, 0xCA40, 0xCA41, 0xCA42, 0xCA43, 0xCA44, 0xCA45, 0xCA46, 0x3131, 0x3132, 0x3133, 0x3134, 0x3135, 0x3136, 0x3137, 0x3138, 0x3139, 0x313A, 0x313B, 0x313C, 0x313D, 0x313E, 0x313F, 0x3140, 0x3141, 0x3142, 0x3143, 0x3144, 0x3145, 0x3146, 0x3147, 0x3148, 0x3149, 0x314A, 0x314B, 0x314C, 0x314D, 0x314E, 0x314F, 0x3150, 0x3151, 0x3152, 0x3153, 0x3154, 0x3155, 0x3156, 0x3157, 0x3158, 0x3159, 0x315A, 0x315B, 0x315C, 0x315D, 0x315E, 0x315F, 0x3160, 0x3161, 0x3162, 0x3163, 0x3164, 0x3165, 0x3166, 0x3167, 0x3168, 0x3169, 0x316A, 0x316B, 0x316C, 0x316D, 0x316E, 0x316F, 0x3170, 0x3171, 0x3172, 0x3173, 0x3174, 0x3175, 0x3176, 0x3177, 0x3178, 0x3179, 0x317A, 0x317B, 0x317C, 0x317D, 0x317E, 0x317F, 0x3180, 0x3181, 0x3182, 0x3183, 0x3184, 0x3185, 0x3186, 0x3187, 0x3188, 0x3189, 0x318A, 0x318B, 0x318C, 0x318D, 0x318E, 0xCA47, 0xCA48, 0xCA49, 0xCA4A, 0xCA4B, 0xCA4E, 0xCA4F, 0xCA51, 0xCA52, 0xCA53, 0xCA55, 0xCA56, 0xCA57, 0xCA58, 0xCA59, 0xCA5A, 0xCA5B, 0xCA5E, 0xCA62, 0xCA63, 0xCA64, 0xCA65, 0xCA66, 0xCA67, 0xCA69, 0xCA6A, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCA6B, 0xCA6C, 0xCA6D, 0xCA6E, 0xCA6F, 0xCA70, 0xCA71, 0xCA72, 0xCA73, 0xCA74, 0xCA75, 0xCA76, 0xCA77, 0xCA78, 0xCA79, 0xCA7A, 0xCA7B, 0xCA7C, 0xCA7E, 0xCA7F, 0xCA80, 0xCA81, 0xCA82, 0xCA83, 0xCA85, 0xCA86, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCA87, 0xCA88, 0xCA89, 0xCA8A, 0xCA8B, 0xCA8C, 0xCA8D, 0xCA8E, 0xCA8F, 0xCA90, 0xCA91, 0xCA92, 0xCA93, 0xCA94, 0xCA95, 0xCA96, 0xCA97, 0xCA99, 0xCA9A, 0xCA9B, 0xCA9C, 0xCA9D, 0xCA9E, 0xCA9F, 0xCAA0, 0xCAA1, 0xCAA2, 0xCAA3, 0xCAA4, 0xCAA5, 0xCAA6, 0xCAA7, 0x2170, 0x2171, 0x2172, 0x2173, 0x2174, 0x2175, 0x2176, 0x2177, 0x2178, 0x2179, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x2160, 0x2161, 0x2162, 0x2163, 0x2164, 0x2165, 0x2166, 0x2167, 0x2168, 0x2169, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, 0x0398, 0x0399, 0x039A, 0x039B, 0x039C, 0x039D, 0x039E, 0x039F, 0x03A0, 0x03A1, 0x03A3, 0x03A4, 0x03A5, 0x03A6, 0x03A7, 0x03A8, 0x03A9, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x03B1, 0x03B2, 0x03B3, 0x03B4, 0x03B5, 0x03B6, 0x03B7, 0x03B8, 0x03B9, 0x03BA, 0x03BB, 0x03BC, 0x03BD, 0x03BE, 0x03BF, 0x03C0, 0x03C1, 0x03C3, 0x03C4, 0x03C5, 0x03C6, 0x03C7, 0x03C8, 0x03C9, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, diff --git a/core/libs/tests/GuestCesDbcs.cpp b/core/libs/tests/GuestCesDbcs.cpp index e8c093f13b..a4bf0db643 100644 --- a/core/libs/tests/GuestCesDbcs.cpp +++ b/core/libs/tests/GuestCesDbcs.cpp @@ -272,8 +272,10 @@ int main() { void* eucJp = sceCesUcsProfileInitEucJpCp51932(eucJpSheet); Require(eucKr == eucKrSheet && uhc == uhcSheet && big5 == big5Sheet && eucJp == eucJpSheet); - RequireTable(sceCesEucKrToUtf8, eucKr, 0x01FF720E36F4AD67ull, 8226); - RequireTable(sceCesUhcToUtf8, uhc, 0x3B861A7E911985ABull, 17048); + RequireTable(sceCesEucKrToUtf8, eucKr, 0x18F4D7FD5B554273ull, 8227); + RequireTable(sceCesUhcToUtf8, uhc, 0x33B12AA5FAB04C43ull, 17049); + RequireScalar(sceCesEucKrToUtf8, eucKr, "\xA2\xE8", "\xE3\x89\xBE"); + RequireScalar(sceCesUhcToUtf8, uhc, "\xA2\xE8", "\xE3\x89\xBE"); RequireTable(sceCesBig5ToUtf8, big5, 0xE4D0D6A8019F35D9ull, 13503); RequireScalar(sceCesEucKrToUtf8, eucKr, "\xB0\xA1", "\xEA\xB0\x80"); diff --git a/docs/dev/debt/libSceCesCs.md b/docs/dev/debt/libSceCesCs.md index 6905596b0a..cda20fb75a 100644 --- a/docs/dev/debt/libSceCesCs.md +++ b/docs/dev/debt/libSceCesCs.md @@ -22,4 +22,4 @@ - [Generic MBCS character and Shift-JIS code helpers](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the UTF-8 and UTF-16 buffer forms' errors and size pointers follow the other converters, unverified. The single-character encoders were measured on a PS5 (`sceCesUtf32ToMbc` over U+0000-U+110001 with each MBCS profile, each encoding's `Ucs2To*` and `Utf32To*` on surrogates and a one-byte target): 0x805C0015 for a surrogate, 0x805C0014 past U+10FFFF, the needed length with `DST_BUFFER_END` and a zero length with any other refusal. `sceCesMbcTo*` and `sceCes*ToMbc` convert one character with any profile the MBCS context accepts; an unmapped character answers `UNASSIGNED_CODE`. `sceCesSJisGetCode`/`PutCode` move one raw Shift-JIS code unchecked; `sceCesSJisCodeToJisX0213` and back use JIS X 0213 annex 1 arithmetic. `sceCesMbcsUcsContextInitCopy` copies the profile pointer. Signatures follow Orbital - [Single-byte profiles](../../../core/libs/prx/libSceCesCs/SingleByte.hpp) (libSceCesCs) - the console's tables for these 27 profiles are not verified. Each returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with bytes 0x00-0x7F unchanged and the high half of a public table: ISO 8859-1, -2, -4, -5, -9, -10, -13, -14, -15 and -16 from [unicode.org](https://unicode.org/Public/MAPPINGS/ISO8859/), code pages 437, 737, 775, 850, 852, 855, 860-863, 865 and 866 from [Microsoft's PC tables](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/PC/), KOI8-R and KOI8-U from [unicode.org](https://unicode.org/Public/MAPPINGS/VENDORS/MISC/), and 1254, 1256 and 1258 from Microsoft's [best-fit tables](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/), whose unassigned bytes map to C1 controls as in Cp1250. Profiles whose public table leaves bytes undefined or maps them to private use still throw - [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 62 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. -- [EUC-KR, UHC and Big5 conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the tables, errors and Big5 duplicate choices are assumed, not measured. `sceCesUcsProfileInitEucKr`, `sceCesUcsProfileInitUhc` and `sceCesUcsProfileInitBig5Cp950` tag the caller's sheet like the EUC-JP profile. The tables are Microsoft's [CP949.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP949.TXT) and [CP950.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP950.TXT); EUC-KR is CP949 limited to bytes `0xA1`-`0xFE`. Bytes below `0x80` are ASCII; the KS X 1003 profile (`sceCes*UcsProfileSetSbcs`) is not implemented. User-defined areas (Private Use Area on Windows) answer `UNASSIGNED_CODE`. Big5 encoding takes the lower of two cells except for U+2550, U+255E, U+2561, U+256A, U+5341 and U+5345, as code page 950 does. The scalar calls to and from UCS-2 and UTF-8/16/32 convert one character as the Shift-JIS and EUC-JP scalar conversions do +- [EUC-KR, UHC and Big5 conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the errors and Big5 duplicate choices are assumed; the decode tables were measured on a PS5 (`sceCesMbcToUtf32` and each `ToUtf8` over every one- and two-byte sequence): UHC and Big5 match, EUC-KR apart from its single bytes. The tables are Microsoft's [CP949.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP949.TXT), plus `0xA2E8` U+327E from KS X 1001:2002, and [CP950.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP950.TXT); EUC-KR is CP949 limited to bytes `0xA1`-`0xFE`, below `0x80` ASCII; the KS X 1003 profile is not implemented. User-defined areas answer `UNASSIGNED_CODE`. Big5 encoding takes the lower of two cells except for U+2550, U+255E, U+2561, U+256A, U+5341 and U+5345, as code page 950 does. The profiles tag the caller's sheet like the EUC-JP profile From 723d932f73394c4576365ae63212481454e9ff7c Mon Sep 17 00:00:00 2001 From: fogydes Date: Wed, 21 Oct 2026 17:37:26 +0530 Subject: [PATCH 379/428] feat(libSceLibcInternal): implement posix_spawn process creation and reaping --- .../prx/libSceLibcInternal/CMakeLists.txt | 2 +- core/libs/prx/libSceLibcInternal/Spawn.cpp | 462 ++++++++++++++++-- core/libs/prx/libc/include/ChildProcesses.hpp | 11 + core/libs/prx/libc/src/Process.cpp | 87 ++++ .../libs/prx/libkernel/System/src/Process.cpp | 49 +- core/libs/tests/GuestPosixSpawn.cpp | 73 ++- docs/dev/debt/libSceLibcInternal.md | 2 +- docs/dev/debt/libc.md | 1 + docs/dev/debt/libkernel.md | 2 +- 9 files changed, 639 insertions(+), 50 deletions(-) create mode 100644 core/libs/prx/libc/include/ChildProcesses.hpp diff --git a/core/libs/prx/libSceLibcInternal/CMakeLists.txt b/core/libs/prx/libSceLibcInternal/CMakeLists.txt index 8087b230e5..96be9446d3 100644 --- a/core/libs/prx/libSceLibcInternal/CMakeLists.txt +++ b/core/libs/prx/libSceLibcInternal/CMakeLists.txt @@ -74,7 +74,7 @@ if(BUILD_TESTING) endif() add_test_executable(guest_posix_spawn_tests ${LIBS_INCLUDE_DIR}/tests/GuestPosixSpawn.cpp) target_include_directories(guest_posix_spawn_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_posix_spawn_tests PRIVATE libSceLibcInternal libc) + target_link_libraries(guest_posix_spawn_tests PRIVATE libSceLibcInternal libc libkernel) configure_windows_unwind(guest_posix_spawn_tests) add_test(NAME guest_posix_spawn COMMAND guest_posix_spawn_tests) set_tests_properties(guest_posix_spawn PROPERTIES TIMEOUT 20) diff --git a/core/libs/prx/libSceLibcInternal/Spawn.cpp b/core/libs/prx/libSceLibcInternal/Spawn.cpp index 72e1bfcaae..93487523a7 100644 --- a/core/libs/prx/libSceLibcInternal/Spawn.cpp +++ b/core/libs/prx/libSceLibcInternal/Spawn.cpp @@ -1,9 +1,16 @@ #include "prx/libc/include/Api.hpp" #include "prx/libSceLibcInternal/include/Api.hpp" #include "prx/libSceLibcInternal/include/Spawn.hpp" +#include "prx/libc/include/ChildProcesses.hpp" #include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/General.hpp" +#ifdef _WIN32 +#include "prx/libc/include/WindowsDescriptor.hpp" +#endif +#include "prx/libkernel/File/include/FileFlags.hpp" +#include #include +#include #include #include #include @@ -11,13 +18,41 @@ #include #include #include +#include +#ifdef _WIN32 +#include +#else +#include +#endif +#if defined(__linux__) +#include +#elif defined(__APPLE__) +#include +#include +#endif + +#if defined(__linux__) +extern char** environ; +#endif namespace { constexpr int NoSuchFile = 2; constexpr int BadDescriptor = 9; constexpr int OutOfMemory = 12; +constexpr int Fault = 14; +constexpr int InvalidArgument = 22; constexpr std::string_view DefaultSearchPath = "/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"; +constexpr std::string_view ApplicationRoot = "/app0/"; +constexpr std::string_view MainExecutable = "/app0/eboot.bin"; +#ifdef _WIN32 +constexpr wchar_t LoadExecParentVariable[] = L"ANYPS5_LOADEXEC_PARENT"; +constexpr wchar_t LoadExecSkipVariable[] = L"ANYPS5_LOADEXEC_ARGV_SKIP="; +#else +constexpr std::uint32_t SpawnFlagsImplemented = 0x03; +#endif +constexpr std::uint32_t SpawnFlagResetIds = 0x01; +constexpr std::uint32_t SpawnFlagSetGroup = 0x02; PosixSpawnAttributes& Attributes(PosixSpawnAttributes* const* attributes, const char* function) { if (attributes == nullptr || *attributes == nullptr) throw std::invalid_argument(std::string(function) + ": uninitialized attributes"); @@ -29,36 +64,6 @@ PosixSpawnFileActions& FileActions(PosixSpawnFileActions* const* actions, const return **actions; } -bool GuestPathExists(const std::string& path) { - std::filesystem::path native; - if (ResolvePathError_nid_no_patch(path.c_str(), native) != 0) return false; - std::error_code error; - return std::filesystem::exists(native, error); -} - -bool ProgramFound(std::string_view name, bool searchesPath) { - if (name.empty()) return false; - if (!searchesPath || name.find('/') != std::string_view::npos) return GuestPathExists(std::string(name)); - const char* variable = getenv_nid_postfix("PATH"); - std::string_view directories = variable != nullptr ? std::string_view(variable) : DefaultSearchPath; - while (true) { - const auto separator = directories.find(':'); - const auto directory = directories.substr(0, separator); - if (GuestPathExists((directory.empty() ? std::string(".") : std::string(directory)) + '/' + std::string(name))) return true; - if (separator == std::string_view::npos) return false; - directories.remove_prefix(separator + 1); - } -} - -int Spawn(const char* function, const char* file, bool searchesPath, PosixSpawnFileActions* const* actions, PosixSpawnAttributes* const* attributes) { - if (file == nullptr) throw std::invalid_argument(std::string(function) + ": null path"); - if (actions != nullptr) FileActions(actions, function); - if (attributes != nullptr) Attributes(attributes, function); - if (!ProgramFound(file, searchesPath)) return NoSuchFile; - NotImplemented_nid_no_patch((std::string(function) + ": process creation").c_str()); - return -1; -} - int Append(PosixSpawnFileActions& actions, PosixSpawnFileAction action) { try { actions.actions.push_back(std::move(action)); @@ -68,6 +73,382 @@ int Append(PosixSpawnFileActions& actions, PosixSpawnFileAction action) { return 0; } +bool GuestPathExists(const std::string& path) { + std::filesystem::path native; + if (ResolvePathError_nid_no_patch(path.c_str(), native) != 0) return false; + std::error_code error; + return std::filesystem::exists(native, error); +} + +bool ProgramFound(std::string_view name, bool searchesPath, std::string& found) { + if (name.empty()) return false; + if (!searchesPath || name.find('/') != std::string_view::npos) { + found = std::string(name); + return GuestPathExists(found); + } + const char* variable = getenv_nid_postfix("PATH"); + std::string_view directories = variable != nullptr ? std::string_view(variable) : DefaultSearchPath; + while (true) { + const auto separator = directories.find(':'); + const auto directory = directories.substr(0, separator); + found = (directory.empty() ? std::string(".") : std::string(directory)) + '/' + std::string(name); + if (GuestPathExists(found)) return true; + if (separator == std::string_view::npos) return false; + directories.remove_prefix(separator + 1); + } +} + +std::string FlattenedExecutableName(const std::string& guest, const char* function) { + std::string_view relative(guest); + if (!relative.starts_with(ApplicationRoot)) NotImplemented_nid_no_patch((std::string(function) + ": executables outside /app0 (" + guest + ")").c_str()); + relative.remove_prefix(ApplicationRoot.size()); + std::string name; + while (true) { + const std::size_t separator = relative.find('/'); + const std::string_view component = relative.substr(0, separator); + if (component.empty() || component == "." || component == "..") NotImplemented_nid_no_patch((std::string(function) + ": a path with an empty, . or .. component (" + guest + ")").c_str()); + name += (name.empty() ? "" : ".") + std::string(component); + if (separator == std::string_view::npos) return name; + relative.remove_prefix(separator + 1); + } +} + +std::filesystem::path OwnExecutable() { +#if defined(_WIN32) + std::wstring own(32768, L'\0'); + const DWORD length = GetModuleFileNameW(nullptr, own.data(), static_cast(own.size())); + if (length == 0 || length >= own.size()) throw std::system_error(GetLastError(), std::system_category(), "posix_spawn: GetModuleFileNameW"); + own.resize(length); + return std::filesystem::path(own); +#elif defined(__APPLE__) + std::uint32_t size = 0; + _NSGetExecutablePath(nullptr, &size); + std::vector executable(size); + std::error_code error; + if (_NSGetExecutablePath(executable.data(), &size) == 0) return std::filesystem::canonical(executable.data(), error); + throw std::runtime_error("posix_spawn: cannot locate the executable"); +#else + std::error_code error; + const auto own = std::filesystem::read_symlink("/proc/self/exe", error); + if (error) throw std::system_error(error, "posix_spawn: cannot read /proc/self/exe"); + return own; +#endif +} + +std::filesystem::path ConvertedExecutableTarget(const char* function, const std::string& guest) { + const auto own = OwnExecutable(); + if (guest == MainExecutable) return own; + std::error_code error; + const auto name = std::filesystem::path(reinterpret_cast((FlattenedExecutableName(guest, function) + own.extension().string()).c_str())); + const auto target = own.parent_path() / name; + if (!std::filesystem::is_regular_file(target, error)) + NotImplemented_nid_no_patch((std::string(function) + ": " + guest + " without its converted executable; relink with --executable " + std::string(guest.substr(ApplicationRoot.size()))).c_str()); + return target; +} + +int HostOpenFlags(std::int32_t sceFlags) { + int f = 0; + const int acc = sceFlags & SCE_KERNEL_O_ACCMODE; + if (acc == SCE_KERNEL_O_RDONLY) f |= 0; + else if (acc == SCE_KERNEL_O_WRONLY) f |= 1; + else if (acc == SCE_KERNEL_O_RDWR) f |= 2; + else throw std::invalid_argument("posix_spawn_file_actions_addopen: invalid access mode"); +#ifdef _WIN32 + if (sceFlags & SCE_KERNEL_O_APPEND) f |= _O_APPEND; + if (sceFlags & SCE_KERNEL_O_CREAT) f |= _O_CREAT; + if (sceFlags & SCE_KERNEL_O_TRUNC) f |= _O_TRUNC; + if (sceFlags & SCE_KERNEL_O_EXCL) f |= _O_EXCL; + f |= _O_BINARY; +#else + if (sceFlags & SCE_KERNEL_O_APPEND) f |= O_APPEND; + if (sceFlags & SCE_KERNEL_O_CREAT) f |= O_CREAT; + if (sceFlags & SCE_KERNEL_O_TRUNC) f |= O_TRUNC; + if (sceFlags & SCE_KERNEL_O_EXCL) f |= O_EXCL; +#endif + return f; +} + +#ifdef _WIN32 +std::wstring Utf16(const std::string& text) { + if (text.empty()) return {}; + const int size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), static_cast(text.size()), nullptr, 0); + if (size <= 0) throw std::runtime_error("posix_spawn: argument is not valid UTF-8"); + std::wstring wide(static_cast(size), L'\0'); + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), static_cast(text.size()), wide.data(), size); + return wide; +} + +std::wstring QuotedArgument(const std::wstring& argument) { + if (!argument.empty() && argument.find_first_of(L" \t\n\v\"") == std::wstring::npos) return argument; + std::wstring quoted = L"\""; + for (std::size_t index = 0;; ++index) { + std::size_t backslashes = 0; + while (index < argument.size() && argument[index] == L'\\') { + ++backslashes; + ++index; + } + if (index == argument.size()) { + quoted.append(backslashes * 2, L'\\'); + break; + } + quoted.append(argument[index] == L'"' ? backslashes * 2 + 1 : backslashes, L'\\'); + quoted.push_back(argument[index]); + } + quoted.push_back(L'"'); + return quoted; +} +#endif + +std::string HexFlag(std::uint32_t value) { + constexpr char digits[] = "0123456789abcdef"; + std::string text; + if (value == 0) text.push_back('0'); + while (value > 0) { + text.insert(text.begin(), digits[value & 0xfu]); + value >>= 4; + } + return "0x" + text; +} + +std::uint32_t SpawnAttributeFlags(PosixSpawnAttributes* const* attributes) { + return attributes != nullptr && *attributes != nullptr ? static_cast(static_cast((*attributes)->flags)) : 0; +} + +#ifdef _WIN32 +struct ChildStreams { + HANDLE handles[3] = {INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE}; +}; + +HANDLE DuplicateInheritable(HANDLE source) { + HANDLE handle = nullptr; + if (!DuplicateHandle(GetCurrentProcess(), source, GetCurrentProcess(), &handle, 0, TRUE, DUPLICATE_SAME_ACCESS)) + throw std::system_error(GetLastError(), std::system_category(), "posix_spawn: duplicating a descriptor"); + return handle; +} + +void CloseRedirectedStreams(ChildStreams& streams) { + for (HANDLE handle : streams.handles) + if (handle != INVALID_HANDLE_VALUE) CloseHandle(handle); +} + +void ReplaceChildStream(ChildStreams& streams, int descriptor, HANDLE source) { + HANDLE previous = streams.handles[descriptor]; + streams.handles[descriptor] = source == INVALID_HANDLE_VALUE ? INVALID_HANDLE_VALUE : DuplicateInheritable(source); + if (previous != INVALID_HANDLE_VALUE) CloseHandle(previous); +} + +int ApplyChildRedirection(ChildStreams& streams, const PosixSpawnFileAction& action) { + switch (action.kind) { + case PosixSpawnFileActionKind::Close: + if (action.descriptor >= 0 && action.descriptor < 3) ReplaceChildStream(streams, action.descriptor, INVALID_HANDLE_VALUE); + return 0; + case PosixSpawnFileActionKind::Duplicate: + if (action.descriptor < 0 || action.newDescriptor < 0) return BadDescriptor; + if (action.newDescriptor > 2) + NotImplemented_nid_no_patch(("posix_spawn: a child descriptor " + std::to_string(action.newDescriptor) + " beyond the standard streams").c_str()); + { + const HANDLE source = WindowsDescriptorHandle_nid_no_patch(action.descriptor); + if (source == INVALID_HANDLE_VALUE) return BadDescriptor; + ReplaceChildStream(streams, action.newDescriptor, source); + } + return 0; + case PosixSpawnFileActionKind::Open: { + if (action.descriptor < 0) return BadDescriptor; + if (action.descriptor > 2) + NotImplemented_nid_no_patch(("posix_spawn: a child descriptor " + std::to_string(action.descriptor) + " beyond the standard streams").c_str()); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(action.path.c_str(), native)) return error; + try { + const int fd = ::_wopen(native.c_str(), HostOpenFlags(action.openFlags), action.mode); + if (fd < 0) return errno; + const HANDLE source = WindowsDescriptorHandle_nid_no_patch(fd); + ReplaceChildStream(streams, action.descriptor, source); + ::_close(fd); + } catch (const std::invalid_argument&) { + return InvalidArgument; + } + return 0; + } + } + return 0; +} + +std::vector ChildEnvironment(char* const* environment) { + std::vector entries; + const auto missesMarkers = [](const std::wstring& entry) { + return entry.compare(0, std::size(LoadExecParentVariable) - 1, LoadExecParentVariable) != 0 && + entry.compare(0, std::size(LoadExecSkipVariable) - 1, LoadExecSkipVariable) != 0; + }; + if (environment != nullptr) + for (std::size_t index = 0; environment[index] != nullptr; ++index) { + const std::wstring entry = Utf16(environment[index]); + if (missesMarkers(entry)) + entries.push_back(entry); + } + else { + const LPWCH block = GetEnvironmentStringsW(); + for (const wchar_t* text = block; *text != L'\0'; ) { + std::size_t length = 0; + while (text[length] != L'\0') ++length; + if (length > 0) { + const std::wstring entry(text, length); + if (missesMarkers(entry)) + entries.push_back(entry); + } + text += length + 1; + } + FreeEnvironmentStringsW(block); + } + entries.push_back(std::wstring(LoadExecSkipVariable) + L"1"); + std::sort(entries.begin(), entries.end(), [](const auto& left, const auto& right) { return _wcsicmp(left.c_str(), right.c_str()) < 0; }); + std::vector block; + for (const auto& entry : entries) { + block.insert(block.end(), entry.begin(), entry.end()); + block.push_back(0); + } + block.push_back(0); + return block; +} + +int SpawnChild(const char* function, const std::string& guest, PosixSpawnFileActions* const* actions, + PosixSpawnAttributes* const* attributes, std::int32_t* processId, char* const* arguments, char* const* environment) { + const auto target = ConvertedExecutableTarget(function, guest); + if (const std::uint32_t attributes_flags = SpawnAttributeFlags(attributes); attributes_flags != 0) + NotImplemented_nid_no_patch(("posix_spawn: the attribute flags " + HexFlag(attributes_flags) + " on this host").c_str()); + ChildStreams streams; + const DWORD identifiers[] = {STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE}; + bool redirected = false; + for (std::size_t index = 0; index < 3; ++index) { + const HANDLE source = GetStdHandle(identifiers[index]); + if (source == nullptr || source == INVALID_HANDLE_VALUE) continue; + streams.handles[index] = DuplicateInheritable(source); + redirected = true; + } + if (actions != nullptr) { + for (const auto& action : (*actions)->actions) { + if (action.kind != PosixSpawnFileActionKind::Close || (action.descriptor >= 0 && action.descriptor < 3)) redirected = true; + const int error = ApplyChildRedirection(streams, action); + if (error != 0) { + CloseRedirectedStreams(streams); + return error; + } + } + } + std::wstring command = L"\"" + target.wstring() + L"\""; + for (std::size_t index = 0; arguments[index] != nullptr; ++index) + command += L" " + QuotedArgument(Utf16(arguments[index])); + if (command.size() >= 32767) throw std::runtime_error("posix_spawn: command line is too long"); + auto environmentBlock = ChildEnvironment(environment); + PROCESS_INFORMATION process{}; + const BOOL created = [&] { + if (!redirected || (streams.handles[0] == INVALID_HANDLE_VALUE && streams.handles[1] == INVALID_HANDLE_VALUE && streams.handles[2] == INVALID_HANDLE_VALUE)) { + STARTUPINFOW plain{}; + plain.cb = sizeof(plain); + return CreateProcessW(const_cast(target.c_str()), command.data(), nullptr, nullptr, FALSE, + environmentBlock.empty() ? 0u : CREATE_UNICODE_ENVIRONMENT, environmentBlock.empty() ? nullptr : environmentBlock.data(), + nullptr, &plain, &process); + } + STARTUPINFOEXW startup{}; + startup.StartupInfo.cb = sizeof(startup); + startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES; + startup.StartupInfo.hStdInput = streams.handles[0]; + startup.StartupInfo.hStdOutput = streams.handles[1]; + startup.StartupInfo.hStdError = streams.handles[2]; + std::vector inherited; + for (HANDLE handle : streams.handles) + if (handle != INVALID_HANDLE_VALUE) inherited.push_back(handle); + SIZE_T bytes = 0; + InitializeProcThreadAttributeList(nullptr, 1, 0, &bytes); + std::vector storage((bytes + sizeof(std::max_align_t) - 1) / sizeof(std::max_align_t)); + auto* list = reinterpret_cast(storage.data()); + if (!InitializeProcThreadAttributeList(list, 1, 0, &bytes) || + !UpdateProcThreadAttribute(list, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, inherited.data(), inherited.size() * sizeof(HANDLE), nullptr, nullptr)) + throw std::system_error(GetLastError(), std::system_category(), "posix_spawn: selecting inherited handles"); + const std::unique_ptr<_PROC_THREAD_ATTRIBUTE_LIST, void (*)(_PROC_THREAD_ATTRIBUTE_LIST*)> cleanup(list, DeleteProcThreadAttributeList); + return CreateProcessW(const_cast(target.c_str()), command.data(), nullptr, nullptr, TRUE, + EXTENDED_STARTUPINFO_PRESENT | (environmentBlock.empty() ? 0u : CREATE_UNICODE_ENVIRONMENT), + environmentBlock.empty() ? nullptr : environmentBlock.data(), nullptr, &startup.StartupInfo, &process); + }(); + if (!created) { + const DWORD error = GetLastError(); + CloseRedirectedStreams(streams); + throw std::system_error(error, std::system_category(), "posix_spawn: creating the converted executable"); + } + CloseRedirectedStreams(streams); + CloseHandle(process.hThread); + RegisterChildProcess_nid_no_patch(process.dwProcessId, static_cast(reinterpret_cast(process.hProcess))); + *processId = static_cast(process.dwProcessId); + return 0; +} +#else +int SpawnChild(const char* function, const std::string& guest, PosixSpawnFileActions* const* actions, + PosixSpawnAttributes* const* attributes, std::int32_t* processId, char* const* arguments, char* const* environment) { + const auto target = ConvertedExecutableTarget(function, guest); + posix_spawnattr_t hostAttributes; + posix_spawnattr_t* usedAttributes = nullptr; + const std::uint32_t flags = SpawnAttributeFlags(attributes); + if ((flags & ~SpawnFlagsImplemented) != 0) + NotImplemented_nid_no_patch(("posix_spawn: the attribute flags " + HexFlag(flags)).c_str()); + if (flags != 0) { + if (posix_spawnattr_init(&hostAttributes) != 0) return Fault; + usedAttributes = &hostAttributes; + if ((flags & SpawnFlagResetIds) != 0) posix_spawnattr_setflags(usedAttributes, POSIX_SPAWN_RESETIDS); + if ((flags & SpawnFlagSetGroup) != 0) { + posix_spawnattr_setflags(usedAttributes, POSIX_SPAWN_SETPGROUP); + posix_spawnattr_setpgroup(usedAttributes, (*attributes)->processGroup); + } + } + posix_spawn_file_actions_t hostActions; + posix_spawn_file_actions_t* usedActions = nullptr; + if (actions != nullptr && !(*actions)->actions.empty()) { + if (posix_spawn_file_actions_init(&hostActions) != 0) { + if (usedAttributes != nullptr) posix_spawnattr_destroy(usedAttributes); + return Fault; + } + usedActions = &hostActions; + } + int result = 0; + if (actions != nullptr && usedActions != nullptr) + for (const auto& action : (*actions)->actions) { + switch (action.kind) { + case PosixSpawnFileActionKind::Close: + result = posix_spawn_file_actions_addclose(usedActions, action.descriptor); + break; + case PosixSpawnFileActionKind::Duplicate: + result = posix_spawn_file_actions_adddup2(usedActions, action.descriptor, action.newDescriptor); + break; + case PosixSpawnFileActionKind::Open: { + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(action.path.c_str(), native)) result = error; + else { + try { + result = posix_spawn_file_actions_addopen(usedActions, action.descriptor, native.c_str(), HostOpenFlags(action.openFlags), action.mode); + } catch (const std::invalid_argument&) { + result = InvalidArgument; + } + } + break; + } + } + if (result != 0) break; + } + if (result == 0) { + char* const* hostEnvironment = environment; +#if defined(__linux__) + if (hostEnvironment == nullptr) hostEnvironment = environ; +#elif defined(__APPLE__) + if (hostEnvironment == nullptr) hostEnvironment = *_NSGetEnviron(); +#endif + pid_t childPid = -1; + result = posix_spawn(&childPid, target.c_str(), usedActions, usedAttributes, arguments, hostEnvironment); + if (result == 0) *processId = static_cast(childPid); + } + if (usedActions != nullptr) posix_spawn_file_actions_destroy(usedActions); + if (usedAttributes != nullptr) posix_spawnattr_destroy(usedAttributes); + return result; +} +#endif + } extern "C" { @@ -198,18 +579,23 @@ int APS5_VABI posix_spawn_file_actions_addclose_nid_postfix(PosixSpawnFileAction return Append(FileActions(actions, "posix_spawn_file_actions_addclose"), {PosixSpawnFileActionKind::Close, descriptor, {}, 0, 0, 0}); } +int SpawnProgram(const char* function, const char* file, bool searchesPath, PosixSpawnFileActions* const* actions, + PosixSpawnAttributes* const* attributes, std::int32_t* processId, char* const* arguments, char* const* environment) { + if (file == nullptr) throw std::invalid_argument(std::string(function) + ": null path"); + if (actions != nullptr) FileActions(actions, function); + if (attributes != nullptr) Attributes(attributes, function); + std::string found; + if (!ProgramFound(file, searchesPath, found)) return NoSuchFile; + if (arguments == nullptr) return Fault; + return SpawnChild(function, found, actions, attributes, processId, arguments, environment); +} + int APS5_VABI posix_spawn_nid_postfix(std::int32_t* processId, const char* path, PosixSpawnFileActions* const* actions, PosixSpawnAttributes* const* attributes, char* const* arguments, char* const* environment) { - (void)processId; - (void)arguments; - (void)environment; - return Spawn("posix_spawn", path, false, actions, attributes); + return SpawnProgram("posix_spawn", path, false, actions, attributes, processId, arguments, environment); } int APS5_VABI posix_spawnp_nid_postfix(std::int32_t* processId, const char* file, PosixSpawnFileActions* const* actions, PosixSpawnAttributes* const* attributes, char* const* arguments, char* const* environment) { - (void)processId; - (void)arguments; - (void)environment; - return Spawn("posix_spawnp", file, true, actions, attributes); + return SpawnProgram("posix_spawnp", file, true, actions, attributes, processId, arguments, environment); } } diff --git a/core/libs/prx/libc/include/ChildProcesses.hpp b/core/libs/prx/libc/include/ChildProcesses.hpp new file mode 100644 index 0000000000..9554bfb475 --- /dev/null +++ b/core/libs/prx/libc/include/ChildProcesses.hpp @@ -0,0 +1,11 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_CHILDPROCESSES_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_CHILDPROCESSES_HPP + +#ifdef _WIN32 +#include + +extern "C" void RegisterChildProcess_nid_no_patch(std::int32_t processId, std::int64_t processHandle); +extern "C" std::int64_t WaitChildProcess_nid_no_patch(std::int32_t processId, unsigned options, int* status); +#endif + +#endif diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index 17f1889ed0..674f48dbbf 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -281,6 +282,92 @@ int APS5_VABI daemon_nid_postfix(int noChdir, int noClose) { return -1; } +#ifdef _WIN32 +namespace { +struct ChildProcess { + std::int32_t processId; + std::int64_t processHandle; +}; + +std::mutex childProcessMutex; +std::vector childProcesses; + +int ChildProcessWaitStatus(DWORD code) { + if (code < 0x80000000u) return static_cast(code & 0xffu) << 8; + switch (code) { + case 0xC0000005u: + case 0xC00000FDu: return 11; + case 0xC000001Du: + case 0xC0000096u: return 4; + case 0x80000003u: return 5; + case 0xC0000094u: + case 0xC0000095u: return 8; + case 0xC0000409u: + case 0xC0000374u: return 6; + default: { + static constexpr char digits[] = "0123456789ABCDEF"; + std::string text(8, '0'); + for (std::size_t index = 0; index < 8; ++index) text[7 - index] = digits[(code >> (index * 4)) & 0xfu]; + NotImplemented_nid_no_patch(("waitpid: a child exited with the status 0x" + text + " is not representable").c_str()); + return 15; + } + return 15; + } +} +} + +extern "C" void RegisterChildProcess_nid_no_patch(std::int32_t processId, std::int64_t processHandle) { + std::lock_guard lock(childProcessMutex); + childProcesses.push_back({processId, processHandle}); +} + +extern "C" std::int64_t WaitChildProcess_nid_no_patch(std::int32_t processId, unsigned options, int* status) { + const bool hang = (options & 1u) != 0; + const auto matches = [&](const ChildProcess& child) { return processId <= 0 || child.processId == processId; }; + { + std::lock_guard lock(childProcessMutex); + if (std::none_of(childProcesses.begin(), childProcesses.end(), matches)) return -2; + } + int encoded = 0; + while (true) { + std::int32_t reaped = 0; + { + std::lock_guard lock(childProcessMutex); + for (std::size_t index = 0; index < childProcesses.size(); ++index) { + auto& child = childProcesses[index]; + if (!matches(child)) continue; + const HANDLE handle = reinterpret_cast(child.processHandle); + if (WaitForSingleObject(handle, 0) != WAIT_OBJECT_0) continue; + DWORD code = 0; + if (!GetExitCodeProcess(handle, &code)) { + *__error_nid_postfix() = 14; + return -1; + } + encoded = ChildProcessWaitStatus(code); + CloseHandle(handle); + reaped = child.processId; + childProcesses.erase(childProcesses.begin() + static_cast(index)); + break; + } + } + if (reaped != 0) { + if (status != nullptr) *status = encoded; + return reaped; + } + if (hang) return 0; + constexpr std::size_t groupSize = MAXIMUM_WAIT_OBJECTS; + std::lock_guard lock(childProcessMutex); + std::vector handles; + for (std::size_t start = 0; start < childProcesses.size(); start += groupSize) { + handles.clear(); + for (std::size_t index = start; index < childProcesses.size() && index < start + groupSize; ++index) + handles.push_back(reinterpret_cast(childProcesses[index].processHandle)); + if (WaitForMultipleObjects(static_cast(handles.size()), handles.data(), FALSE, 250) != WAIT_TIMEOUT) break; + } + } +} +#endif + } namespace { diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index cce4173934..1f508c6dea 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -22,6 +22,11 @@ #include #endif +#ifdef _WIN32 +#include "prx/libc/include/ChildProcesses.hpp" +#else +#include +#endif #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/ApplicationHeap.hpp" @@ -424,10 +429,46 @@ int APS5_VABI system_nid_postfix(const char* command) { } int APS5_VABI waitpid_nid_postfix(int pid, int* status, int options) { - (void)pid; - (void)status; - *__error_nid_postfix() = (static_cast(options) & ~freebsdWaitOptions) != 0 ? errnoInvalidArgument : errnoNoChild; - return -1; + if ((static_cast(options) & ~freebsdWaitOptions) != 0) { + *__error_nid_postfix() = errnoInvalidArgument; + return -1; + } +#ifdef _WIN32 + int encoded = 0; + const std::int64_t reaped = WaitChildProcess_nid_no_patch(pid, static_cast(options), &encoded); + if (reaped == -2) { + *__error_nid_postfix() = errnoNoChild; + return -1; + } + if (reaped == -1) return -1; + if (reaped == 0) return 0; + if (status != nullptr) *status = encoded; + return static_cast(reaped); +#else + int encoded = 0; + const int result = ::waitpid(pid, &encoded, options & (WNOHANG | WUNTRACED | WCONTINUED)); + if (result < 0) { + *__error_nid_postfix() = errno; + return -1; + } + if (result == 0) return 0; + if (WIFSIGNALED(encoded) || WIFSTOPPED(encoded)) { + const int hostSignal = WIFSIGNALED(encoded) ? WTERMSIG(encoded) : WSTOPSIG(encoded); + switch (hostSignal) { + case 7: encoded = (encoded & ~0x7f) | 10; break; + case 10: encoded = (encoded & ~0x7f) | 30; break; + case 12: encoded = (encoded & ~0x7f) | 31; break; + case 16: + case 29: + case 30: + NotImplemented_nid_no_patch("waitpid: a child died with a signal without a guest equivalent"); + return -1; + default: break; + } + } + if (status != nullptr) *status = encoded; + return result; +#endif } int APS5_VABI setrlimit_nid_postfix(int resource, const GuestResourceLimit* limit) { diff --git a/core/libs/tests/GuestPosixSpawn.cpp b/core/libs/tests/GuestPosixSpawn.cpp index ecfb146655..fe9f58131a 100644 --- a/core/libs/tests/GuestPosixSpawn.cpp +++ b/core/libs/tests/GuestPosixSpawn.cpp @@ -11,6 +11,14 @@ #include #include #include +#include +#ifdef _WIN32 +#include +#endif + +#if defined(__linux__) +extern char** environ; +#endif static void Require(bool condition, int line) { if (!condition) { @@ -150,29 +158,29 @@ static void CheckSpawn() { std::int32_t processId = 77; Check(ReportsNotImplemented([&] { posix_spawn_nid_postfix(&processId, existing.c_str(), &actions, &attributes, arguments, environment); }, - "posix_spawn: process creation not implemented")); + "posix_spawn: executables outside /app0 (" + existing + ") not implemented")); Check(posix_spawn_nid_postfix(&processId, missing.c_str(), &actions, &attributes, arguments, environment) == NoSuchFile); Check(posix_spawn_nid_postfix(&processId, "", nullptr, nullptr, arguments, nullptr) == NoSuchFile); Check(posix_spawn_nid_postfix(&processId, "tool.elf", nullptr, nullptr, arguments, nullptr) == NoSuchFile); Check(setenv_nid_postfix("PATH", bothDirectories.c_str(), 1) == 0); Check(ReportsNotImplemented([&] { posix_spawnp_nid_postfix(&processId, "tool.elf", &actions, &attributes, arguments, nullptr); }, - "posix_spawnp: process creation not implemented")); + "posix_spawnp: executables outside /app0 (" + existing + ") not implemented")); Check(posix_spawnp_nid_postfix(&processId, "missing.elf", &actions, &attributes, arguments, nullptr) == NoSuchFile); Check(posix_spawnp_nid_postfix(&processId, "", nullptr, nullptr, arguments, nullptr) == NoSuchFile); Check(setenv_nid_postfix("PATH", emptyOnly.c_str(), 1) == 0); Check(posix_spawnp_nid_postfix(&processId, "tool.elf", nullptr, nullptr, arguments, nullptr) == NoSuchFile); Check(ReportsNotImplemented([&] { posix_spawnp_nid_postfix(&processId, existing.c_str(), nullptr, nullptr, arguments, nullptr); }, - "posix_spawnp: process creation not implemented")); + "posix_spawnp: executables outside /app0 (" + existing + ") not implemented")); Check(posix_spawnp_nid_postfix(&processId, missing.c_str(), nullptr, nullptr, arguments, nullptr) == NoSuchFile); Check(setenv_nid_postfix("PATH", (emptyOnly + ":").c_str(), 1) == 0); Check(posix_spawnp_nid_postfix(&processId, "tool.elf", nullptr, nullptr, arguments, nullptr) == NoSuchFile); Check(chdir_nid_postfix(guest.c_str()) == 0); Check(ReportsNotImplemented([&] { posix_spawnp_nid_postfix(&processId, "tool.elf", nullptr, nullptr, arguments, nullptr); }, - "posix_spawnp: process creation not implemented")); + "posix_spawnp: executables outside /app0 (./tool.elf) not implemented")); Check(ReportsNotImplemented([&] { posix_spawn_nid_postfix(&processId, "tool.elf", nullptr, nullptr, arguments, nullptr); }, - "posix_spawn: process creation not implemented")); + "posix_spawn: executables outside /app0 (tool.elf) not implemented")); Check(chdir_nid_postfix("/") == 0); Check(unsetenv_nid_postfix("PATH") == 0); @@ -190,8 +198,63 @@ static void CheckSpawn() { std::filesystem::remove_all(directory); } +static std::filesystem::path OwnExecutablePath() { +#ifdef _WIN32 + std::wstring own(32768, L'\0'); + const DWORD length = GetModuleFileNameW(nullptr, own.data(), static_cast(own.size())); + own.resize(length); + return std::filesystem::path(own); +#else + std::error_code error; + return std::filesystem::read_symlink("/proc/self/exe", error); +#endif +} + +static void CheckSpawnedChild(const char* role, int expectedCode) { + const auto own = OwnExecutablePath(); + const auto target = own.parent_path() / std::filesystem::path(reinterpret_cast(("child.bin" + own.extension().string()).c_str())); + std::error_code error; + std::filesystem::copy_file(own, target, std::filesystem::copy_options::overwrite_existing, error); + Check(!error); + const auto app0 = std::filesystem::current_path() / "app0"; + Check(std::filesystem::create_directories(app0)); + { std::ofstream file(app0 / "child.bin"); file << "child"; } + + char program[] = "/app0/child.bin"; + char* arguments[] = {program, nullptr}; + char roleValue[64]; + std::strcpy(roleValue, "ANYPS5_POSIX_SPAWN_ROLE="); + std::strcat(roleValue, role); + char* environment[] = {roleValue, nullptr}; + std::int32_t processId = 0; + Check(posix_spawn_nid_postfix(&processId, program, nullptr, nullptr, arguments, environment) == 0); + Check(processId > 0); + + int status = -1; + if (std::strcmp(role, "child-sleep") == 0) + Check(waitpid_nid_postfix(processId, &status, 1) == 0); + Check(waitpid_nid_postfix(processId, &status, 0) == processId); + Check((status & 0x7f) == 0 && ((status >> 8) & 0xff) == expectedCode); + + status = -1; + Check(waitpid_nid_postfix(processId, &status, 0) == -1 && *__error_nid_postfix() == 10); + + std::filesystem::remove(target, error); + std::filesystem::remove_all(app0, error); +} + int main() { + if (const char* role = std::getenv("ANYPS5_POSIX_SPAWN_ROLE")) { + if (std::strcmp(role, "child-exit-42") == 0) std::_Exit(42); + if (std::strcmp(role, "child-sleep") == 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + std::_Exit(43); + } + std::_Exit(99); + } CheckAttributes(); CheckFileActions(); CheckSpawn(); + CheckSpawnedChild("child-exit-42", 42); + CheckSpawnedChild("child-sleep", 43); } diff --git a/docs/dev/debt/libSceLibcInternal.md b/docs/dev/debt/libSceLibcInternal.md index b1cb33da7a..45b5ced78b 100644 --- a/docs/dev/debt/libSceLibcInternal.md +++ b/docs/dev/debt/libSceLibcInternal.md @@ -13,7 +13,7 @@ - [Thread exit destructors](../../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped - [fdopendir](../../../core/libs/prx/libSceLibcInternal/Directory.cpp) (libSceLibcInternal) follows FreeBSD 11's [`fdopendir`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/opendir.c#L67-L74): it sets `FD_CLOEXEC` on the descriptor first, and the stream starts at the descriptor's current position instead of the first entry. On Windows the stream skips the entries the descriptor's cursor (`lseek` or `getdirentries`) has passed, which assumes that the descriptor and the stream list a directory in the same host order; on Linux the stream is opened on a duplicate of the descriptor, which shares its position. The Linux path is not run by the tests here. As in FreeBSD, a descriptor that is not open fails with `EBADF`, one that is not a directory with `ENOTDIR` (both leave it open, and `FD_CLOEXEC` stays set on the latter), and `closedir` closes it - [fstatvfs](../../../core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp) (libSceLibcInternal) converts `_fstatfs` to the 88-byte `struct statvfs` as FreeBSD 9.2's `sfs2svfs` does (`f_bsize` from the transfer size, `f_frsize` from the block size, `f_favail` equal to `f_ffree`, `f_fsid` 0, `ST_RDONLY` and `ST_NOSUID` only). Differences: `f_namemax` is the `statfs` value, not `fpathconf(_PC_NAME_MAX)`; negative `f_bavail` and `f_ffree` wrap to unsigned without `EOVERFLOW`, as the same-width copy in FreeBSD does; inode counts are 0 on Windows, inherited from `_fstatfs`; a null output returns `EFAULT` after the descriptor is validated, where FreeBSD would fault. Layout and conversion are unverified against the PS5 library (PS5MC imports `fstatvfs` but was not run) -- [posix_spawn and posix_spawnp](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) create no process. FreeBSD 11.4 returns the error the child's exec leaves ([`_posix_spawn_thr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/posix_spawn.c#L227-L231), [`do_posix_spawn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/posix_spawn.c#L295-L303)), and `posix_spawnp` searches `PATH`, or `_PATH_DEFPATH` when it is unset, ending with `ENOENT` when no directory holds the program ([`_execvpe`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/exec.c#L272-L281), [`execvPe`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/exec.c#L257-L260)). So when the program is not in the guest file system both return `ENOENT` and leave the process ID unwritten; when it is there they throw. PPSA99305 (Dusklight) reaches this when SDL looks for `zenity` to show a file dialog. The attributes and file actions are not applied, so one that would fail in the child is not reported, and whether the console's application sandbox lets the child be created at all (FreeBSD returns that error first) is unverified. The attribute and file action calls keep FreeBSD 11.4's objects. Null or uninitialized objects, which FreeBSD dereferences, throw. +- [posix_spawn and posix_spawnp](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) create a native child process running the converted executable of the guest path, which must be under `/app0/` (the running image for `/app0/eboot.bin`) with its flattened name beside the running image; a `posix_spawnp` `PATH` search follows FreeBSD 11's `_execvpe` (`_PATH_DEFPATH` when unset, ending with `ENOENT`), and the file-actions and attributes from FreeBSD 11.4's objects are applied to the child. `argv` null returns `EFAULT` (14); a null `envp` gives the child the parent's environment. Guest process IDs are the host's, so `waitpid` sees the child. PPSA99305 (Dusklight) reaches this when SDL looks for `zenity` to show a file dialog. Limitations remain: any real child executes the already-converted image of the requested path, so a guest spawning a program it builds or edits at run time would run a stale conversion (FreeBSD execs the file fresh); file actions on descriptors 3 and above record FreeBSD 11.4's errors on all hosts but only descriptors 0 to 2 are actually redirected on Windows, where a spawned child also gets stdin, stdout and stderr from handle duplication, so a title's stdio layout cannot differ from the parent's; the attribute flags `POSIX_SPAWN_RESETIDS` and `POSIX_SPAWN_SETPGROUP` are only applied on Linux, every other flag or a nonzero flag set on Windows throws, where FreeBSD would apply signal handling in the child. How the console's application sandbox limits child creation is unverified. The attribute and file action calls keep FreeBSD 11.4's objects. Null or uninitialized objects, which FreeBSD dereferences, throw. - [getopt, getopt_long](../../../core/libs/prx/libSceLibcInternal/Getopt.cpp) (libSceLibcInternal) follow FreeBSD 11 [getopt.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt.c) and [getopt_long.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt_long.c) with `GNU_COMPATIBLE`: `getopt` does not permute, `getopt_long` permutes unless `POSIXLY_CORRECT` is set in the guest environment or the option string starts with `+`, and the exported `opterr`, `optind`, `optopt`, `optreset` and `optarg` start at 1, 1, 0, 0 and null. Warnings go to the guest's current `stderr` prefixed with `__progname`, where FreeBSD's `warnx` keeps the `stderr` of its first call. A null `getopt` option string, a null argument vector or argument, a negative `optind` and an `optind` moved inside a pending permutation throw where FreeBSD faults. `getopt_long_only` is not exported. That the console's `libSceLibcInternal` uses this FreeBSD code is unverified - [posix_spawnattr_getflags, getpgroup, getschedparam, getschedpolicy, getsigdefault, getsigmask, setpgroup, setschedparam, setschedpolicy and setsigmask](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) read and write the 0x30-byte attribute object at the offsets of the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x8D860 to 0x8D910: flags at 0, process group at 4, scheduling parameter at 8, policy at 0xC, default and masked signal sets at 0x10 and 0x20), which never fail there and return 0; the layout matched on a PS5 (firmware 13.60). Uninitialized attributes and null pointers, which the console dereferences, throw as the existing setters do - [sys_siglist and sys_nsig](../../../core/libs/prx/libSceLibcInternal/SignalList.cpp) (libSceLibcInternal) hold the 32 descriptions and the count of [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/siglist.c#L73-L107) `siglist.c` (`NSIG` 32, signal 0 to `SIGUSR2`); the console's table was not read diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index abc3a959b9..4bbd9c40d9 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -172,3 +172,4 @@ - [collate vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, NID `ruZtIwbCFjk`) is nine zeroed `uintptr_t` slots matching `st_size` 72 on the title `libc.prx` from PPSA16833 (offset-to-top, typeinfo, two destructors, `_Incref`/`_Decref`, and three `do_*` virtuals). Facet calls through the object fault until those methods are wired; `_Getcat` still installs the host `g_collateVtable` for facets it builds. - [num_get::id, time_get::id and the num_get vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) are data objects, as the public SDK's libSceLibcInternal table declares them and as the other facet ids and the `num_put` and `codecvt` vtables are kept: the ids start at zero and the vtable is 17 zeroed slots, the 136-byte size of `KfcTPbeaOqg` in the console's libSceLibcInternal, whose relocations fill the offset-to-top and typeinfo entries, the two destructors, `locale::facet::_Incref` and `_Decref`, and the eleven `do_get` of Dinkumware's crt.vc12 `xlocnum`, so a guest resolves the symbols and reads data, and a facet call through the vtable faults. libSceAmpr exported the three as throwing functions, which a data consumer reads as code; its qualified imports fall back to libc. - [printf family conversions](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) - Windows `FormatWindows` follows the title `libc.prx` `_Printf` measured from PPSA13934, PPSA07923, PPSA05916 and PPSA04158: `%q`, `%D`, `%O` and `%U` are unknown conversions; an unknown character is printed with its leading `%`, and width, `-` and discarded length modifiers behave as measured (`%5y` is ` %y`, `%hhz` is `%z`). `%%` takes a width. A trailing `%` reads past the format's NUL and is not reproduced; it throws. `#`, precision and a `0` without a width are ignored, as a payload process on a PS5 (firmware 13.60) printed `%#k` and `%.2k` as `%k`; `+`, space, `'` and `0` with a width on an unknown conversion are unverified and throw. On Linux a format with an unknown conversion also goes to this formatter +- [child process registry](../../../core/libs/prx/libc/src/Process.cpp) (libc) - `RegisterChildProcess_nid_no_patch` and `WaitChildProcess_nid_no_patch` track the native handles of children created by `posix_spawn` ([libSceLibcInternal](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp)) so libkernel can reap them on Windows, where the host has no `waitpid`. The registry lives in libc because libSceLibcInternal imports libc but not libkernel. `RegisterChildProcess_nid_no_patch` stores the process ID and handle and closes the handle when `WaitChildProcess_nid_no_patch` reaps it; only reaped handles are closed, and a process that exits without waiting leaks them. Registration without a live child is not validated, and a duplicate process ID is accepted. These exports are internal glue for libkernel, not console ABI: their behavior is unverified against any PS5 library and a title importing their NIDs directly is not expected. diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 4e1851fb4e..3b837feff9 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -193,7 +193,7 @@ - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified - [pthread_setaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - follows [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_affinity.c#L41-L62) libthr over `kern_cpuset_setaffinity` for the calling thread and any live guest thread: a size other than the 32-byte `cpuset_t` returns `ERANGE`, an unknown or finished thread `ESRCH`. Bits outside `sceKernelGetAvailableCpumask` are dropped and a mask with none of those bits returns `EDEADLK`, as `cpuset_setaffinity` does; the affinity is recorded on the guest thread and not applied to the host thread, and the console result is unverified - [dup and dup2](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` of a guest socket onto descriptors 0 to 2, whose host streams libc writes without the socket table. Guest sockets share one socket object between the duplicates; the numbers come from the host descriptor table like those of files. The errno values for descriptor exhaustion are not verified against the console. On Windows a guest `dup2` onto descriptor 1 or 2 keeps host logging on an `_fdopen` of a duplicate, and the guest `FILE` stays on the descriptor that was replaced; opening that duplicate throws (#7791) -- [waitpid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns -1 with `ECHILD` (`EINVAL` for unknown options): nothing here creates a child process; child processes would need a real `waitpid`. +- [waitpid and wait](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) reap child processes created by `posix_spawn` ([libSceLibcInternal](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp)), whose guest process IDs are the host's on both hosts. On Linux the call passes through to the host `waitpid`; an exit status is returned verbatim, and when a child dies of a signal the number is translated to the guest's BSD numbering (7 bus, 10 user1, 12 user2 become 10, 30, 31; a child dead of Linux signals 16, 29 or 30, which have no guest equivalent, throws). On Windows a child's exit code below 0x80000000 encodes the guest exit byte the way `exit` does, and a structured-exception code maps to signal deaths: access violation and stack overflow to `SIGSEGV`, illegal instruction to `SIGILL`, breakpoint to `SIGTRAP`, integer and float division to `SIGFPE`, fastfail and heap corruption to `SIGABRT`; any other NT status throws. A stop or continue status is never produced on Windows, and unknown `options` bits fail with `EINVAL` exactly as before. There are no other child producers, so calls without a `posix_spawn` child keep `ECHILD`. - [execv](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported, the same as `execvp`. PPSA97358 imports it and fails to load without it. - [execve](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws like `execvp`: replacing the process image is not supported. The console behavior for invalid arguments and executable path lookups is not modeled. - [sysarch](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `AMD64_GET_FSBASE`, `AMD64_GET_GSBASE` and `AMD64_SET_GSBASE` follow FreeBSD 11's amd64 [`sysarch`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c) on Linux x86-64 hosts, through the host's `arch_prctl`. The GS base belongs to the calling thread, a base at or above `0x800000000000` (`VM_MAXUSER_ADDRESS`) returns `EINVAL`, and a null argument returns `EFAULT`. `AMD64_SET_FSBASE` throws, since guest and host code share the thread's FS base for their TLS. In a payload process on a PS5 (firmware 12.70), `I386_GET_FSBASE` (7) and `I386_GET_GSBASE` (9) wrote 4 bytes, the low 32 bits of the FS and GS bases, and failed with `EFAULT` for a null argument; here they do the same on Linux x86-64 hosts. There a GS base set by one thread left the main thread's unchanged, and threads made with `pthread_create` and `scePthreadCreate` started with their creator's GS base; here a guest thread's host thread is created by the creating thread, and Linux's [`copy_thread`](https://github.com/torvalds/linux/blob/v6.6/arch/x86/kernel/process.c#L182-L186) gives a new thread its creator's GS base. The operation numbers FreeBSD 11's amd64 `sysarch` has no case for returned `EINVAL` there for the nine tried, each with a null argument (2, 5, 6, 12, 127, 133, 134, 1000 and -1; 6 is `I386_VM86`, which its header names), as its [default case](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c#L324-L325) does; here every such number returns `EINVAL` on every host; the numbers other than those nine follow that default case and were not measured. `AMD64_GET_XFPUSTATE` (132) returned 0 there for lengths 0, 64, 128, 256 and 512 (512 was also the largest length a search accepted) and failed with `EINVAL` for 1024, 2048, 4096 and 8192 and with `EFAULT` for a null argument or address, and `I386_GET_XFPUSTATE` (11) failed with `EFAULT` for a null argument; both throw here. The other operations FreeBSD 11's amd64 `sysarch` handles, and every operation it handles on other hosts (Windows keeps its TEB in GS), throw. From df18d830723b06c47cdc37b7164fcc65986df78f Mon Sep 17 00:00:00 2001 From: fogydes Date: Wed, 21 Oct 2026 18:18:38 +0530 Subject: [PATCH 380/428] docs: add the spawn role variable and shorten the waitpid debt --- docs/dev/ENVIRONMENT.md | 1 + docs/dev/debt/libSceLibcInternal.md | 2 +- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 1d8ecab03a..c1910303e6 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -10,6 +10,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `ANYPS5_JOB_MANAGER_HOST` | presence | `core/libs/tests/JobManagerReference.cpp` | | runs the reference adapter against the host fallback instead of the loaded guest module | | `ANYPS5_LOADEXEC_PARENT` | string | `core/libs/tests/GuestLoadExecHost.cpp` | | variable the Windows entry stub reads; its wide name is built in `core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp` | | `ANYPS5_LOADEXEC_TEST_ROOT` | string | `core/libs/tests/GuestLoadExecHost.cpp` | | directory the load-exec host test reads its guest executable from | +| `ANYPS5_POSIX_SPAWN_ROLE` | string | `core/libs/tests/GuestPosixSpawn.cpp` | | role of the test's own copy spawned as a `posix_spawn` child, which exits or sleeps on it and otherwise fails | | `ANYPS5_TEST_QUOTED_ARGV0` | presence | `core/libs/tests/GuestLoadExecHost.cpp` | | makes the load-exec test's first argument one with quotes, a backslash and non-ASCII text | | `APS5_EBOOT` | string | `core/libs/tests/GuestSystemExit.cpp` | | host path of the executable launched for /app0/eboot.bin, defaulting to the running executable | | `APS5_SKIP_TIMING_TESTS` | presence | `core/libs/tests/PreciseWait.cpp` | | treats the timing tests' runner as preempted, so they skip without probing the clock | diff --git a/docs/dev/debt/libSceLibcInternal.md b/docs/dev/debt/libSceLibcInternal.md index 45b5ced78b..3ac4b14e2d 100644 --- a/docs/dev/debt/libSceLibcInternal.md +++ b/docs/dev/debt/libSceLibcInternal.md @@ -13,7 +13,7 @@ - [Thread exit destructors](../../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped - [fdopendir](../../../core/libs/prx/libSceLibcInternal/Directory.cpp) (libSceLibcInternal) follows FreeBSD 11's [`fdopendir`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/opendir.c#L67-L74): it sets `FD_CLOEXEC` on the descriptor first, and the stream starts at the descriptor's current position instead of the first entry. On Windows the stream skips the entries the descriptor's cursor (`lseek` or `getdirentries`) has passed, which assumes that the descriptor and the stream list a directory in the same host order; on Linux the stream is opened on a duplicate of the descriptor, which shares its position. The Linux path is not run by the tests here. As in FreeBSD, a descriptor that is not open fails with `EBADF`, one that is not a directory with `ENOTDIR` (both leave it open, and `FD_CLOEXEC` stays set on the latter), and `closedir` closes it - [fstatvfs](../../../core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp) (libSceLibcInternal) converts `_fstatfs` to the 88-byte `struct statvfs` as FreeBSD 9.2's `sfs2svfs` does (`f_bsize` from the transfer size, `f_frsize` from the block size, `f_favail` equal to `f_ffree`, `f_fsid` 0, `ST_RDONLY` and `ST_NOSUID` only). Differences: `f_namemax` is the `statfs` value, not `fpathconf(_PC_NAME_MAX)`; negative `f_bavail` and `f_ffree` wrap to unsigned without `EOVERFLOW`, as the same-width copy in FreeBSD does; inode counts are 0 on Windows, inherited from `_fstatfs`; a null output returns `EFAULT` after the descriptor is validated, where FreeBSD would fault. Layout and conversion are unverified against the PS5 library (PS5MC imports `fstatvfs` but was not run) -- [posix_spawn and posix_spawnp](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) create a native child process running the converted executable of the guest path, which must be under `/app0/` (the running image for `/app0/eboot.bin`) with its flattened name beside the running image; a `posix_spawnp` `PATH` search follows FreeBSD 11's `_execvpe` (`_PATH_DEFPATH` when unset, ending with `ENOENT`), and the file-actions and attributes from FreeBSD 11.4's objects are applied to the child. `argv` null returns `EFAULT` (14); a null `envp` gives the child the parent's environment. Guest process IDs are the host's, so `waitpid` sees the child. PPSA99305 (Dusklight) reaches this when SDL looks for `zenity` to show a file dialog. Limitations remain: any real child executes the already-converted image of the requested path, so a guest spawning a program it builds or edits at run time would run a stale conversion (FreeBSD execs the file fresh); file actions on descriptors 3 and above record FreeBSD 11.4's errors on all hosts but only descriptors 0 to 2 are actually redirected on Windows, where a spawned child also gets stdin, stdout and stderr from handle duplication, so a title's stdio layout cannot differ from the parent's; the attribute flags `POSIX_SPAWN_RESETIDS` and `POSIX_SPAWN_SETPGROUP` are only applied on Linux, every other flag or a nonzero flag set on Windows throws, where FreeBSD would apply signal handling in the child. How the console's application sandbox limits child creation is unverified. The attribute and file action calls keep FreeBSD 11.4's objects. Null or uninitialized objects, which FreeBSD dereferences, throw. +- [posix_spawn and posix_spawnp](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) create a native child running the requested program's converted image, which must be under `/app0/` (`/app0/eboot.bin` maps to the running image); `posix_spawnp` searches `PATH` as FreeBSD 11's `_execvpe`, and the FreeBSD 11.4 file actions and attributes are applied to the child. A null `argv` returns `EFAULT`; a null `envp` inherits the parent environment; guest process IDs are the host's. Limits: the child executes the conversion made at relink time; Windows redirects only descriptors 0 to 2 and starts the child on the parent's std handles; Linux applies only `POSIX_SPAWN_RESETIDS` and `POSIX_SPAWN_SETPGROUP`, other flags or a nonzero set on Windows throw. The console sandbox's effect on child creation is unverified, and null or uninitialized objects, which FreeBSD dereferences, throw. PPSA99305 (Dusklight) reaches this when SDL looks for `zenity` - [getopt, getopt_long](../../../core/libs/prx/libSceLibcInternal/Getopt.cpp) (libSceLibcInternal) follow FreeBSD 11 [getopt.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt.c) and [getopt_long.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt_long.c) with `GNU_COMPATIBLE`: `getopt` does not permute, `getopt_long` permutes unless `POSIXLY_CORRECT` is set in the guest environment or the option string starts with `+`, and the exported `opterr`, `optind`, `optopt`, `optreset` and `optarg` start at 1, 1, 0, 0 and null. Warnings go to the guest's current `stderr` prefixed with `__progname`, where FreeBSD's `warnx` keeps the `stderr` of its first call. A null `getopt` option string, a null argument vector or argument, a negative `optind` and an `optind` moved inside a pending permutation throw where FreeBSD faults. `getopt_long_only` is not exported. That the console's `libSceLibcInternal` uses this FreeBSD code is unverified - [posix_spawnattr_getflags, getpgroup, getschedparam, getschedpolicy, getsigdefault, getsigmask, setpgroup, setschedparam, setschedpolicy and setsigmask](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) read and write the 0x30-byte attribute object at the offsets of the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x8D860 to 0x8D910: flags at 0, process group at 4, scheduling parameter at 8, policy at 0xC, default and masked signal sets at 0x10 and 0x20), which never fail there and return 0; the layout matched on a PS5 (firmware 13.60). Uninitialized attributes and null pointers, which the console dereferences, throw as the existing setters do - [sys_siglist and sys_nsig](../../../core/libs/prx/libSceLibcInternal/SignalList.cpp) (libSceLibcInternal) hold the 32 descriptions and the count of [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/siglist.c#L73-L107) `siglist.c` (`NSIG` 32, signal 0 to `SIGUSR2`); the console's table was not read diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 3b837feff9..0a805b5b93 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -193,7 +193,7 @@ - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified - [pthread_setaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - follows [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_affinity.c#L41-L62) libthr over `kern_cpuset_setaffinity` for the calling thread and any live guest thread: a size other than the 32-byte `cpuset_t` returns `ERANGE`, an unknown or finished thread `ESRCH`. Bits outside `sceKernelGetAvailableCpumask` are dropped and a mask with none of those bits returns `EDEADLK`, as `cpuset_setaffinity` does; the affinity is recorded on the guest thread and not applied to the host thread, and the console result is unverified - [dup and dup2](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` of a guest socket onto descriptors 0 to 2, whose host streams libc writes without the socket table. Guest sockets share one socket object between the duplicates; the numbers come from the host descriptor table like those of files. The errno values for descriptor exhaustion are not verified against the console. On Windows a guest `dup2` onto descriptor 1 or 2 keeps host logging on an `_fdopen` of a duplicate, and the guest `FILE` stays on the descriptor that was replaced; opening that duplicate throws (#7791) -- [waitpid and wait](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) reap child processes created by `posix_spawn` ([libSceLibcInternal](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp)), whose guest process IDs are the host's on both hosts. On Linux the call passes through to the host `waitpid`; an exit status is returned verbatim, and when a child dies of a signal the number is translated to the guest's BSD numbering (7 bus, 10 user1, 12 user2 become 10, 30, 31; a child dead of Linux signals 16, 29 or 30, which have no guest equivalent, throws). On Windows a child's exit code below 0x80000000 encodes the guest exit byte the way `exit` does, and a structured-exception code maps to signal deaths: access violation and stack overflow to `SIGSEGV`, illegal instruction to `SIGILL`, breakpoint to `SIGTRAP`, integer and float division to `SIGFPE`, fastfail and heap corruption to `SIGABRT`; any other NT status throws. A stop or continue status is never produced on Windows, and unknown `options` bits fail with `EINVAL` exactly as before. There are no other child producers, so calls without a `posix_spawn` child keep `ECHILD`. +- [waitpid and wait](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) reap children of [posix_spawn](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp), whose guest process IDs are the host's. On Linux the host `waitpid` runs; a status is returned verbatim, and a signal death is translated to the guest numbering (7, 10, 12 to 10, 30, 31; Linux 16, 29, 30 have no guest equivalent and throw). On Windows an exit code below 0x80000000 keeps the guest `exit` byte layout, and a structured-exception code maps to `SIGSEGV`, `SIGILL`, `SIGTRAP`, `SIGFPE` or `SIGABRT` (access violation and stack overflow, illegal instruction, breakpoint, division, fastfail and heap corruption); any other NT status throws. No stop or continue status is produced on Windows, and unknown `options` bits fail with `EINVAL` as before. Without a `posix_spawn` child the call keeps returning `ECHILD`. - [execv](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported, the same as `execvp`. PPSA97358 imports it and fails to load without it. - [execve](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws like `execvp`: replacing the process image is not supported. The console behavior for invalid arguments and executable path lookups is not modeled. - [sysarch](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `AMD64_GET_FSBASE`, `AMD64_GET_GSBASE` and `AMD64_SET_GSBASE` follow FreeBSD 11's amd64 [`sysarch`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c) on Linux x86-64 hosts, through the host's `arch_prctl`. The GS base belongs to the calling thread, a base at or above `0x800000000000` (`VM_MAXUSER_ADDRESS`) returns `EINVAL`, and a null argument returns `EFAULT`. `AMD64_SET_FSBASE` throws, since guest and host code share the thread's FS base for their TLS. In a payload process on a PS5 (firmware 12.70), `I386_GET_FSBASE` (7) and `I386_GET_GSBASE` (9) wrote 4 bytes, the low 32 bits of the FS and GS bases, and failed with `EFAULT` for a null argument; here they do the same on Linux x86-64 hosts. There a GS base set by one thread left the main thread's unchanged, and threads made with `pthread_create` and `scePthreadCreate` started with their creator's GS base; here a guest thread's host thread is created by the creating thread, and Linux's [`copy_thread`](https://github.com/torvalds/linux/blob/v6.6/arch/x86/kernel/process.c#L182-L186) gives a new thread its creator's GS base. The operation numbers FreeBSD 11's amd64 `sysarch` has no case for returned `EINVAL` there for the nine tried, each with a null argument (2, 5, 6, 12, 127, 133, 134, 1000 and -1; 6 is `I386_VM86`, which its header names), as its [default case](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c#L324-L325) does; here every such number returns `EINVAL` on every host; the numbers other than those nine follow that default case and were not measured. `AMD64_GET_XFPUSTATE` (132) returned 0 there for lengths 0, 64, 128, 256 and 512 (512 was also the largest length a search accepted) and failed with `EINVAL` for 1024, 2048, 4096 and 8192 and with `EFAULT` for a null argument or address, and `I386_GET_XFPUSTATE` (11) failed with `EFAULT` for a null argument; both throw here. The other operations FreeBSD 11's amd64 `sysarch` handles, and every operation it handles on other hosts (Windows keeps its TEB in GS), throw. From 19a3d60aeda4af1e4876706134cd961f44954992 Mon Sep 17 00:00:00 2001 From: fogydes Date: Fri, 23 Oct 2026 12:16:46 +0530 Subject: [PATCH 381/428] fix(libSceLibcInternal): declare _NSGetExecutablePath on Apple hosts --- core/libs/prx/libSceLibcInternal/Spawn.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceLibcInternal/Spawn.cpp b/core/libs/prx/libSceLibcInternal/Spawn.cpp index 93487523a7..4c7bd815b2 100644 --- a/core/libs/prx/libSceLibcInternal/Spawn.cpp +++ b/core/libs/prx/libSceLibcInternal/Spawn.cpp @@ -28,6 +28,7 @@ #include #elif defined(__APPLE__) #include +#include #include #endif From 0514307ab844d241289568f0ed41759e02d91939 Mon Sep 17 00:00:00 2001 From: fogydes Date: Sat, 24 Oct 2026 14:21:57 +0530 Subject: [PATCH 382/428] fix(libkernel): translate waitpid child statuses to the guest numbering --- core/libs/prx/libc/src/Process.cpp | 31 +++---- .../libs/prx/libkernel/System/src/Process.cpp | 26 ++++-- core/libs/tests/GuestPosixSpawn.cpp | 86 +++++++++++++++++-- docs/dev/debt/libc.md | 2 +- docs/dev/debt/libkernel.md | 2 +- 5 files changed, 114 insertions(+), 33 deletions(-) diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index 674f48dbbf..ec68c7e8b3 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -293,7 +293,6 @@ std::mutex childProcessMutex; std::vector childProcesses; int ChildProcessWaitStatus(DWORD code) { - if (code < 0x80000000u) return static_cast(code & 0xffu) << 8; switch (code) { case 0xC0000005u: case 0xC00000FDu: return 11; @@ -304,14 +303,7 @@ int ChildProcessWaitStatus(DWORD code) { case 0xC0000095u: return 8; case 0xC0000409u: case 0xC0000374u: return 6; - default: { - static constexpr char digits[] = "0123456789ABCDEF"; - std::string text(8, '0'); - for (std::size_t index = 0; index < 8; ++index) text[7 - index] = digits[(code >> (index * 4)) & 0xfu]; - NotImplemented_nid_no_patch(("waitpid: a child exited with the status 0x" + text + " is not representable").c_str()); - return 15; - } - return 15; + default: return static_cast(code & 0xffu) << 8; } } } @@ -322,7 +314,7 @@ extern "C" void RegisterChildProcess_nid_no_patch(std::int32_t processId, std::i } extern "C" std::int64_t WaitChildProcess_nid_no_patch(std::int32_t processId, unsigned options, int* status) { - const bool hang = (options & 1u) != 0; + const bool noWait = (options & 1u) != 0; const auto matches = [&](const ChildProcess& child) { return processId <= 0 || child.processId == processId; }; { std::lock_guard lock(childProcessMutex); @@ -354,15 +346,18 @@ extern "C" std::int64_t WaitChildProcess_nid_no_patch(std::int32_t processId, un if (status != nullptr) *status = encoded; return reaped; } - if (hang) return 0; - constexpr std::size_t groupSize = MAXIMUM_WAIT_OBJECTS; - std::lock_guard lock(childProcessMutex); + if (noWait) return 0; std::vector handles; - for (std::size_t start = 0; start < childProcesses.size(); start += groupSize) { - handles.clear(); - for (std::size_t index = start; index < childProcesses.size() && index < start + groupSize; ++index) - handles.push_back(reinterpret_cast(childProcesses[index].processHandle)); - if (WaitForMultipleObjects(static_cast(handles.size()), handles.data(), FALSE, 250) != WAIT_TIMEOUT) break; + { + const std::lock_guard lock(childProcessMutex); + if (childProcesses.empty()) continue; + for (const auto& child : childProcesses) handles.push_back(reinterpret_cast(child.processHandle)); + } + constexpr std::size_t groupSize = MAXIMUM_WAIT_OBJECTS; + bool timedOut = true; + for (std::size_t start = 0; start < handles.size() && timedOut; start += groupSize) { + const std::size_t count = std::min(handles.size() - start, groupSize); + timedOut = WaitForMultipleObjects(static_cast(count), handles.data() + start, FALSE, 250) == WAIT_TIMEOUT; } } } diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 1f508c6dea..42a4926f4c 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -445,26 +445,38 @@ int APS5_VABI waitpid_nid_postfix(int pid, int* status, int options) { if (status != nullptr) *status = encoded; return static_cast(reaped); #else + unsigned hostOptions = static_cast(options) & 3u; + if ((static_cast(options) & 4u) != 0) hostOptions |= static_cast(WCONTINUED); int encoded = 0; - const int result = ::waitpid(pid, &encoded, options & (WNOHANG | WUNTRACED | WCONTINUED)); + const int result = ::waitpid(pid, &encoded, static_cast(hostOptions)); if (result < 0) { *__error_nid_postfix() = errno; return -1; } if (result == 0) return 0; - if (WIFSIGNALED(encoded) || WIFSTOPPED(encoded)) { + if (WIFCONTINUED(encoded)) { + encoded = 0x13; + } else if (WIFSIGNALED(encoded) || WIFSTOPPED(encoded)) { const int hostSignal = WIFSIGNALED(encoded) ? WTERMSIG(encoded) : WSTOPSIG(encoded); + int guestSignal = hostSignal; switch (hostSignal) { - case 7: encoded = (encoded & ~0x7f) | 10; break; - case 10: encoded = (encoded & ~0x7f) | 30; break; - case 12: encoded = (encoded & ~0x7f) | 31; break; + case 7: guestSignal = 10; break; + case 10: guestSignal = 30; break; + case 12: guestSignal = 31; break; + case 17: guestSignal = 20; break; + case 18: guestSignal = 19; break; + case 19: guestSignal = 17; break; + case 20: guestSignal = 18; break; + case 23: guestSignal = 16; break; + case 29: guestSignal = 23; break; + case 31: guestSignal = 12; break; case 16: - case 29: case 30: - NotImplemented_nid_no_patch("waitpid: a child died with a signal without a guest equivalent"); + NotImplemented_nid_no_patch("waitpid: a child stopped or died with a signal without a guest equivalent"); return -1; default: break; } + encoded = WIFSTOPPED(encoded) ? ((encoded & ~0xff00) | (guestSignal << 8)) : ((encoded & ~0x7f) | guestSignal); } if (status != nullptr) *status = encoded; return result; diff --git a/core/libs/tests/GuestPosixSpawn.cpp b/core/libs/tests/GuestPosixSpawn.cpp index fe9f58131a..016a233efe 100644 --- a/core/libs/tests/GuestPosixSpawn.cpp +++ b/core/libs/tests/GuestPosixSpawn.cpp @@ -3,6 +3,7 @@ #include "prx/libSceLibcInternal/include/Spawn.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include @@ -14,6 +15,8 @@ #include #ifdef _WIN32 #include +#else +extern "C" int kill(int, int); #endif #if defined(__linux__) @@ -210,14 +213,14 @@ static std::filesystem::path OwnExecutablePath() { #endif } -static void CheckSpawnedChild(const char* role, int expectedCode) { +static std::int32_t SpawnChild(const char* role) { const auto own = OwnExecutablePath(); const auto target = own.parent_path() / std::filesystem::path(reinterpret_cast(("child.bin" + own.extension().string()).c_str())); std::error_code error; std::filesystem::copy_file(own, target, std::filesystem::copy_options::overwrite_existing, error); Check(!error); const auto app0 = std::filesystem::current_path() / "app0"; - Check(std::filesystem::create_directories(app0)); + Check(std::filesystem::create_directories(app0) || std::filesystem::exists(app0)); { std::ofstream file(app0 / "child.bin"); file << "child"; } char program[] = "/app0/child.bin"; @@ -229,27 +232,85 @@ static void CheckSpawnedChild(const char* role, int expectedCode) { std::int32_t processId = 0; Check(posix_spawn_nid_postfix(&processId, program, nullptr, nullptr, arguments, environment) == 0); Check(processId > 0); + return processId; +} +static void CleanupChild() { + std::error_code error; + std::filesystem::remove(OwnExecutablePath().parent_path() / std::filesystem::path(reinterpret_cast(("child.bin" + OwnExecutablePath().extension().string()).c_str())), error); + std::filesystem::remove_all(std::filesystem::current_path() / "app0", error); +} + +static void CheckSpawnedChild(const char* role, int expectedCode) { + const std::int32_t processId = SpawnChild(role); int status = -1; if (std::strcmp(role, "child-sleep") == 0) Check(waitpid_nid_postfix(processId, &status, 1) == 0); Check(waitpid_nid_postfix(processId, &status, 0) == processId); Check((status & 0x7f) == 0 && ((status >> 8) & 0xff) == expectedCode); - status = -1; Check(waitpid_nid_postfix(processId, &status, 0) == -1 && *__error_nid_postfix() == 10); - - std::filesystem::remove(target, error); - std::filesystem::remove_all(app0, error); + CleanupChild(); } +#ifndef _WIN32 +static void CheckStoppedChild() { + const std::int32_t processId = SpawnChild("child-stop"); + int status = -1; + Check(waitpid_nid_postfix(processId, &status, 2) == processId); + Check((status & 0x7f) == 0x7f && (status >> 8) == 17); + Check(kill(processId, SIGCONT) == 0); + Check(waitpid_nid_postfix(processId, &status, 4) == processId && status == 0x13); + Check(waitpid_nid_postfix(processId, &status, 0) == processId); + Check((status & 0x7f) == 0 && ((status >> 8) & 0xff) == 44); + Check(waitpid_nid_postfix(processId, &status, 0) == -1 && *__error_nid_postfix() == 10); + CleanupChild(); +} + +static void CheckKilledChild(const char* role, int guestSignal) { + const std::int32_t processId = SpawnChild(role); + int status = -1; + Check(waitpid_nid_postfix(processId, &status, 0) == processId); + Check((status & 0x7f) == guestSignal); + Check(waitpid_nid_postfix(processId, &status, 0) == -1 && *__error_nid_postfix() == 10); + CleanupChild(); +} +#else +static void CheckTerminatedChild(const char* role, unsigned code, int guestSignal) { + const std::int32_t processId = SpawnChild(role); + const HANDLE process = OpenProcess(PROCESS_TERMINATE, FALSE, static_cast(processId)); + Check(process != nullptr); + Check(TerminateProcess(process, code) != 0); + CloseHandle(process); + int status = -1; + Check(waitpid_nid_postfix(processId, &status, 0) == processId); + Check(code == 0xFFFFFFFFu ? ((status & 0x7f) == 0 && ((status >> 8) & 0xff) == 255) + : ((status & 0x7f) == guestSignal && (status & 0x80) == 0)); + Check(waitpid_nid_postfix(processId, &status, 0) == -1 && *__error_nid_postfix() == 10); + CleanupChild(); +} +#endif + int main() { if (const char* role = std::getenv("ANYPS5_POSIX_SPAWN_ROLE")) { if (std::strcmp(role, "child-exit-42") == 0) std::_Exit(42); + if (std::strcmp(role, "child-exit-neg") == 0) std::exit(-1); +#ifndef _WIN32 + if (std::strcmp(role, "child-stop") == 0) { + std::raise(SIGSTOP); + std::_Exit(44); + } + if (std::strcmp(role, "child-kill-bus") == 0) std::raise(SIGBUS); + if (std::strcmp(role, "child-kill-sys") == 0) std::raise(SIGSYS); +#endif if (std::strcmp(role, "child-sleep") == 0) { std::this_thread::sleep_for(std::chrono::milliseconds(250)); std::_Exit(43); } + if (std::strcmp(role, "child-laze") == 0) { + std::this_thread::sleep_for(std::chrono::seconds(30)); + std::_Exit(47); + } std::_Exit(99); } CheckAttributes(); @@ -257,4 +318,17 @@ int main() { CheckSpawn(); CheckSpawnedChild("child-exit-42", 42); CheckSpawnedChild("child-sleep", 43); + CheckSpawnedChild("child-exit-neg", 255); +#ifndef _WIN32 + CheckStoppedChild(); + CheckKilledChild("child-kill-bus", 10); + CheckKilledChild("child-kill-sys", 12); +#else + CheckTerminatedChild("child-laze", 0xC0000005u, 11); + CheckTerminatedChild("child-laze", 0xC0000096u, 4); + CheckTerminatedChild("child-laze", 0x80000003u, 5); + CheckTerminatedChild("child-laze", 0xC0000094u, 8); + CheckTerminatedChild("child-laze", 0xC0000409u, 6); + CheckTerminatedChild("child-laze", 0xFFFFFFFFu, 0); +#endif } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 4bbd9c40d9..cb047dfa8d 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -172,4 +172,4 @@ - [collate vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, NID `ruZtIwbCFjk`) is nine zeroed `uintptr_t` slots matching `st_size` 72 on the title `libc.prx` from PPSA16833 (offset-to-top, typeinfo, two destructors, `_Incref`/`_Decref`, and three `do_*` virtuals). Facet calls through the object fault until those methods are wired; `_Getcat` still installs the host `g_collateVtable` for facets it builds. - [num_get::id, time_get::id and the num_get vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) are data objects, as the public SDK's libSceLibcInternal table declares them and as the other facet ids and the `num_put` and `codecvt` vtables are kept: the ids start at zero and the vtable is 17 zeroed slots, the 136-byte size of `KfcTPbeaOqg` in the console's libSceLibcInternal, whose relocations fill the offset-to-top and typeinfo entries, the two destructors, `locale::facet::_Incref` and `_Decref`, and the eleven `do_get` of Dinkumware's crt.vc12 `xlocnum`, so a guest resolves the symbols and reads data, and a facet call through the vtable faults. libSceAmpr exported the three as throwing functions, which a data consumer reads as code; its qualified imports fall back to libc. - [printf family conversions](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) - Windows `FormatWindows` follows the title `libc.prx` `_Printf` measured from PPSA13934, PPSA07923, PPSA05916 and PPSA04158: `%q`, `%D`, `%O` and `%U` are unknown conversions; an unknown character is printed with its leading `%`, and width, `-` and discarded length modifiers behave as measured (`%5y` is ` %y`, `%hhz` is `%z`). `%%` takes a width. A trailing `%` reads past the format's NUL and is not reproduced; it throws. `#`, precision and a `0` without a width are ignored, as a payload process on a PS5 (firmware 13.60) printed `%#k` and `%.2k` as `%k`; `+`, space, `'` and `0` with a width on an unknown conversion are unverified and throw. On Linux a format with an unknown conversion also goes to this formatter -- [child process registry](../../../core/libs/prx/libc/src/Process.cpp) (libc) - `RegisterChildProcess_nid_no_patch` and `WaitChildProcess_nid_no_patch` track the native handles of children created by `posix_spawn` ([libSceLibcInternal](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp)) so libkernel can reap them on Windows, where the host has no `waitpid`. The registry lives in libc because libSceLibcInternal imports libc but not libkernel. `RegisterChildProcess_nid_no_patch` stores the process ID and handle and closes the handle when `WaitChildProcess_nid_no_patch` reaps it; only reaped handles are closed, and a process that exits without waiting leaks them. Registration without a live child is not validated, and a duplicate process ID is accepted. These exports are internal glue for libkernel, not console ABI: their behavior is unverified against any PS5 library and a title importing their NIDs directly is not expected. +- [child process registry](../../../core/libs/prx/libc/src/Process.cpp) (libc) - `RegisterChildProcess_nid_no_patch` and `WaitChildProcess_nid_no_patch` track the native handles of children created by `posix_spawn` ([libSceLibcInternal](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp)) so libkernel can reap them on Windows, where the host has no `waitpid`. The registry lives in libc because libSceLibcInternal imports libc but not libkernel. `RegisterChildProcess_nid_no_patch` stores the process ID and handle; only reaped handles are closed, and a process that exits without waiting leaks them. Registration without a live child is not validated, and a duplicate process ID is accepted. The decoder maps recognized structured-exception codes to guest signals, keeping the guest `exit` byte layout for anything else. These exports are internal glue for libkernel, not console ABI: their behavior is unverified against any PS5 library and a title importing their NIDs directly is not expected. diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 0a805b5b93..eca92b4827 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -193,7 +193,7 @@ - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified - [pthread_setaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - follows [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_affinity.c#L41-L62) libthr over `kern_cpuset_setaffinity` for the calling thread and any live guest thread: a size other than the 32-byte `cpuset_t` returns `ERANGE`, an unknown or finished thread `ESRCH`. Bits outside `sceKernelGetAvailableCpumask` are dropped and a mask with none of those bits returns `EDEADLK`, as `cpuset_setaffinity` does; the affinity is recorded on the guest thread and not applied to the host thread, and the console result is unverified - [dup and dup2](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` of a guest socket onto descriptors 0 to 2, whose host streams libc writes without the socket table. Guest sockets share one socket object between the duplicates; the numbers come from the host descriptor table like those of files. The errno values for descriptor exhaustion are not verified against the console. On Windows a guest `dup2` onto descriptor 1 or 2 keeps host logging on an `_fdopen` of a duplicate, and the guest `FILE` stays on the descriptor that was replaced; opening that duplicate throws (#7791) -- [waitpid and wait](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) reap children of [posix_spawn](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp), whose guest process IDs are the host's. On Linux the host `waitpid` runs; a status is returned verbatim, and a signal death is translated to the guest numbering (7, 10, 12 to 10, 30, 31; Linux 16, 29, 30 have no guest equivalent and throw). On Windows an exit code below 0x80000000 keeps the guest `exit` byte layout, and a structured-exception code maps to `SIGSEGV`, `SIGILL`, `SIGTRAP`, `SIGFPE` or `SIGABRT` (access violation and stack overflow, illegal instruction, breakpoint, division, fastfail and heap corruption); any other NT status throws. No stop or continue status is produced on Windows, and unknown `options` bits fail with `EINVAL` as before. Without a `posix_spawn` child the call keeps returning `ECHILD`. +- [waitpid and wait](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) reaps [posix_spawn](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) children, whose guest process IDs are the host's. On Linux the host call runs: a stopped child's signal in bits 8-15 and a killed child's signal in bits 0-6 translate to the guest numbering (7, 10, 12, 17, 18, 19, 20, 23, 29, 31 to 10, 30, 31, 20, 19, 17, 18, 16, 23, 12; Linux 16 and 30 are unmapped and throw), a continued status becomes 0x13, and guest `WCONTINUED` 4 becomes Linux 8. On Windows an unrecognized exit code keeps the guest `exit` byte layout, so `exit(-1)` reads as 255, and a structured-exception code maps to `SIGSEGV`, `SIGILL`, `SIGTRAP`, `SIGFPE` or `SIGABRT` (access violation and stack overflow, illegal instruction, breakpoint, division, fastfail). Windows produces no stop or continue status, and unknown `options` bits fail with `EINVAL`. Without a spawned child the call keeps returning `ECHILD`. - [execv](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported, the same as `execvp`. PPSA97358 imports it and fails to load without it. - [execve](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws like `execvp`: replacing the process image is not supported. The console behavior for invalid arguments and executable path lookups is not modeled. - [sysarch](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `AMD64_GET_FSBASE`, `AMD64_GET_GSBASE` and `AMD64_SET_GSBASE` follow FreeBSD 11's amd64 [`sysarch`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c) on Linux x86-64 hosts, through the host's `arch_prctl`. The GS base belongs to the calling thread, a base at or above `0x800000000000` (`VM_MAXUSER_ADDRESS`) returns `EINVAL`, and a null argument returns `EFAULT`. `AMD64_SET_FSBASE` throws, since guest and host code share the thread's FS base for their TLS. In a payload process on a PS5 (firmware 12.70), `I386_GET_FSBASE` (7) and `I386_GET_GSBASE` (9) wrote 4 bytes, the low 32 bits of the FS and GS bases, and failed with `EFAULT` for a null argument; here they do the same on Linux x86-64 hosts. There a GS base set by one thread left the main thread's unchanged, and threads made with `pthread_create` and `scePthreadCreate` started with their creator's GS base; here a guest thread's host thread is created by the creating thread, and Linux's [`copy_thread`](https://github.com/torvalds/linux/blob/v6.6/arch/x86/kernel/process.c#L182-L186) gives a new thread its creator's GS base. The operation numbers FreeBSD 11's amd64 `sysarch` has no case for returned `EINVAL` there for the nine tried, each with a null argument (2, 5, 6, 12, 127, 133, 134, 1000 and -1; 6 is `I386_VM86`, which its header names), as its [default case](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c#L324-L325) does; here every such number returns `EINVAL` on every host; the numbers other than those nine follow that default case and were not measured. `AMD64_GET_XFPUSTATE` (132) returned 0 there for lengths 0, 64, 128, 256 and 512 (512 was also the largest length a search accepted) and failed with `EINVAL` for 1024, 2048, 4096 and 8192 and with `EFAULT` for a null argument or address, and `I386_GET_XFPUSTATE` (11) failed with `EFAULT` for a null argument; both throw here. The other operations FreeBSD 11's amd64 `sysarch` handles, and every operation it handles on other hosts (Windows keeps its TEB in GS), throw. From bde7cc210a266265d565a31e399a295b576b6d61 Mon Sep 17 00:00:00 2001 From: fogydes Date: Sat, 24 Oct 2026 21:29:43 +0530 Subject: [PATCH 383/428] fix(libkernel): report ECHILD to a Windows waiter another thread reaps first --- core/libs/prx/libc/src/Process.cpp | 9 ++++++--- core/libs/tests/GuestPosixSpawn.cpp | 27 +++++++++++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index ec68c7e8b3..8d7c88c0d9 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -346,13 +346,16 @@ extern "C" std::int64_t WaitChildProcess_nid_no_patch(std::int32_t processId, un if (status != nullptr) *status = encoded; return reaped; } - if (noWait) return 0; std::vector handles; { const std::lock_guard lock(childProcessMutex); - if (childProcesses.empty()) continue; - for (const auto& child : childProcesses) handles.push_back(reinterpret_cast(child.processHandle)); + for (const auto& child : childProcesses) { + if (!matches(child)) continue; + handles.push_back(reinterpret_cast(child.processHandle)); + } } + if (handles.empty()) return -2; + if (noWait) return 0; constexpr std::size_t groupSize = MAXIMUM_WAIT_OBJECTS; bool timedOut = true; for (std::size_t start = 0; start < handles.size() && timedOut; start += groupSize) { diff --git a/core/libs/tests/GuestPosixSpawn.cpp b/core/libs/tests/GuestPosixSpawn.cpp index 016a233efe..aee6806757 100644 --- a/core/libs/tests/GuestPosixSpawn.cpp +++ b/core/libs/tests/GuestPosixSpawn.cpp @@ -253,6 +253,32 @@ static void CheckSpawnedChild(const char* role, int expectedCode) { CleanupChild(); } +static void CheckConcurrentWaiters() { + const std::int32_t processId = SpawnChild("child-sleep"); + int statusA = -1; + int statusB = -1; + int errorA = 0; + int errorB = 0; + int resultA = 0; + int resultB = 0; + const auto waiter = [processId](int* status, int* error) { + const int result = waitpid_nid_postfix(processId, status, 0); + *error = *__error_nid_postfix(); + return result; + }; + std::thread threadA([&] { resultA = waiter(&statusA, &errorA); }); + std::thread threadB([&] { resultB = waiter(&statusB, &errorB); }); + threadA.join(); + threadB.join(); + const bool reapedA = resultA == processId; + const bool reapedB = resultB == processId; + Check(reapedA != reapedB); + const int reapedStatus = reapedA ? statusA : statusB; + Check((reapedStatus & 0x7f) == 0 && ((reapedStatus >> 8) & 0xff) == 43); + Check((reapedA ? errorB : errorA) == 10); + CleanupChild(); +} + #ifndef _WIN32 static void CheckStoppedChild() { const std::int32_t processId = SpawnChild("child-stop"); @@ -319,6 +345,7 @@ int main() { CheckSpawnedChild("child-exit-42", 42); CheckSpawnedChild("child-sleep", 43); CheckSpawnedChild("child-exit-neg", 255); + CheckConcurrentWaiters(); #ifndef _WIN32 CheckStoppedChild(); CheckKilledChild("child-kill-bus", 10); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index eca92b4827..4d6760bd05 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -193,7 +193,7 @@ - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified - [pthread_setaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - follows [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_affinity.c#L41-L62) libthr over `kern_cpuset_setaffinity` for the calling thread and any live guest thread: a size other than the 32-byte `cpuset_t` returns `ERANGE`, an unknown or finished thread `ESRCH`. Bits outside `sceKernelGetAvailableCpumask` are dropped and a mask with none of those bits returns `EDEADLK`, as `cpuset_setaffinity` does; the affinity is recorded on the guest thread and not applied to the host thread, and the console result is unverified - [dup and dup2](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` of a guest socket onto descriptors 0 to 2, whose host streams libc writes without the socket table. Guest sockets share one socket object between the duplicates; the numbers come from the host descriptor table like those of files. The errno values for descriptor exhaustion are not verified against the console. On Windows a guest `dup2` onto descriptor 1 or 2 keeps host logging on an `_fdopen` of a duplicate, and the guest `FILE` stays on the descriptor that was replaced; opening that duplicate throws (#7791) -- [waitpid and wait](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) reaps [posix_spawn](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) children, whose guest process IDs are the host's. On Linux the host call runs: a stopped child's signal in bits 8-15 and a killed child's signal in bits 0-6 translate to the guest numbering (7, 10, 12, 17, 18, 19, 20, 23, 29, 31 to 10, 30, 31, 20, 19, 17, 18, 16, 23, 12; Linux 16 and 30 are unmapped and throw), a continued status becomes 0x13, and guest `WCONTINUED` 4 becomes Linux 8. On Windows an unrecognized exit code keeps the guest `exit` byte layout, so `exit(-1)` reads as 255, and a structured-exception code maps to `SIGSEGV`, `SIGILL`, `SIGTRAP`, `SIGFPE` or `SIGABRT` (access violation and stack overflow, illegal instruction, breakpoint, division, fastfail). Windows produces no stop or continue status, and unknown `options` bits fail with `EINVAL`. Without a spawned child the call keeps returning `ECHILD`. +- [waitpid and wait](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) reaps [posix_spawn](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) children, whose guest process IDs are the host's. On Linux the host call runs: the guest signal numbering applies in bits 8-15 for a stopped child and bits 0-6 for a killed one (7, 10, 12, 17, 18, 19, 20, 23, 29, 31 to 10, 30, 31, 20, 19, 17, 18, 16, 23, 12; Linux 16 and 30 are unmapped and throw), a continued status becomes 0x13, and guest `WCONTINUED` 4 becomes Linux 8. On Windows an unrecognized exit code keeps the guest `exit` byte layout, so `exit(-1)` reads as 255, and a structured-exception code maps to `SIGSEGV`, `SIGILL`, `SIGTRAP`, `SIGFPE` or `SIGABRT` (access violation/stack overflow, illegal instruction, breakpoint, division, fastfail). Windows has no stop/continue status; unknown `options` bits fail with `EINVAL`. Without an unreaped matching child, even after another thread reaps it, the call gives `ECHILD`. - [execv](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported, the same as `execvp`. PPSA97358 imports it and fails to load without it. - [execve](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws like `execvp`: replacing the process image is not supported. The console behavior for invalid arguments and executable path lookups is not modeled. - [sysarch](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `AMD64_GET_FSBASE`, `AMD64_GET_GSBASE` and `AMD64_SET_GSBASE` follow FreeBSD 11's amd64 [`sysarch`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c) on Linux x86-64 hosts, through the host's `arch_prctl`. The GS base belongs to the calling thread, a base at or above `0x800000000000` (`VM_MAXUSER_ADDRESS`) returns `EINVAL`, and a null argument returns `EFAULT`. `AMD64_SET_FSBASE` throws, since guest and host code share the thread's FS base for their TLS. In a payload process on a PS5 (firmware 12.70), `I386_GET_FSBASE` (7) and `I386_GET_GSBASE` (9) wrote 4 bytes, the low 32 bits of the FS and GS bases, and failed with `EFAULT` for a null argument; here they do the same on Linux x86-64 hosts. There a GS base set by one thread left the main thread's unchanged, and threads made with `pthread_create` and `scePthreadCreate` started with their creator's GS base; here a guest thread's host thread is created by the creating thread, and Linux's [`copy_thread`](https://github.com/torvalds/linux/blob/v6.6/arch/x86/kernel/process.c#L182-L186) gives a new thread its creator's GS base. The operation numbers FreeBSD 11's amd64 `sysarch` has no case for returned `EINVAL` there for the nine tried, each with a null argument (2, 5, 6, 12, 127, 133, 134, 1000 and -1; 6 is `I386_VM86`, which its header names), as its [default case](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c#L324-L325) does; here every such number returns `EINVAL` on every host; the numbers other than those nine follow that default case and were not measured. `AMD64_GET_XFPUSTATE` (132) returned 0 there for lengths 0, 64, 128, 256 and 512 (512 was also the largest length a search accepted) and failed with `EINVAL` for 1024, 2048, 4096 and 8192 and with `EFAULT` for a null argument or address, and `I386_GET_XFPUSTATE` (11) failed with `EFAULT` for a null argument; both throw here. The other operations FreeBSD 11's amd64 `sysarch` handles, and every operation it handles on other hosts (Windows keeps its TEB in GS), throw. From 4a913acbd65bb98c5c28c60c6a23b7f12bf305ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 25 Oct 2026 11:13:56 +0100 Subject: [PATCH 384/428] fix(libSceLibcInternal): declare waitpid in the spawn test through the libkernel header --- core/libs/tests/GuestPosixSpawn.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/tests/GuestPosixSpawn.cpp b/core/libs/tests/GuestPosixSpawn.cpp index aee6806757..8ad98fbb5f 100644 --- a/core/libs/tests/GuestPosixSpawn.cpp +++ b/core/libs/tests/GuestPosixSpawn.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/Api.hpp" #include "prx/libSceLibcInternal/include/Api.hpp" #include "prx/libSceLibcInternal/include/Spawn.hpp" +#include "prx/libkernel/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include From 2bfcf6166a96930224c9e459039c7048a2a40945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 25 Oct 2026 13:57:38 +0100 Subject: [PATCH 385/428] fix(libSceLibcInternal): give the spawned test child the parent environment plus its role Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestPosixSpawn.cpp | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/core/libs/tests/GuestPosixSpawn.cpp b/core/libs/tests/GuestPosixSpawn.cpp index 8ad98fbb5f..d71e652950 100644 --- a/core/libs/tests/GuestPosixSpawn.cpp +++ b/core/libs/tests/GuestPosixSpawn.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #ifdef _WIN32 #include #else @@ -226,12 +227,21 @@ static std::int32_t SpawnChild(const char* role) { char program[] = "/app0/child.bin"; char* arguments[] = {program, nullptr}; - char roleValue[64]; - std::strcpy(roleValue, "ANYPS5_POSIX_SPAWN_ROLE="); - std::strcat(roleValue, role); - char* environment[] = {roleValue, nullptr}; + std::vector entries; +#ifdef _WIN32 + char** const parentEnvironment = _environ; +#else + char** const parentEnvironment = environ; +#endif + for (char** entry = parentEnvironment; entry != nullptr && *entry != nullptr; ++entry) + entries.push_back(*entry); + entries.push_back(std::string("ANYPS5_POSIX_SPAWN_ROLE=") + role); + std::vector environment; + for (auto& entry : entries) + environment.push_back(entry.data()); + environment.push_back(nullptr); std::int32_t processId = 0; - Check(posix_spawn_nid_postfix(&processId, program, nullptr, nullptr, arguments, environment) == 0); + Check(posix_spawn_nid_postfix(&processId, program, nullptr, nullptr, arguments, environment.data()) == 0); Check(processId > 0); return processId; } From 9d710abf9d5f57390b08262aa53aec68e10a238f Mon Sep 17 00:00:00 2001 From: hypermist Date: Thu, 22 Oct 2026 03:14:39 +1300 Subject: [PATCH 386/428] fix(libs): return what the firmware returns from 29 more calls it does not implement Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libSceComposite/Export.cpp | 9 ++---- .../prx/libSceFsInternalForVsh/Export.cpp | 6 ++-- core/libs/prx/libSceHttpCache/Export.cpp | 1 - core/libs/prx/libScePlayReady/Export.cpp | 3 +- core/libs/prx/libSceSdma/Export.cpp | 11 ++----- core/libs/prx/libSceSsl/Export.cpp | 2 -- core/libs/prx/libSceVisionManager/Export.cpp | 31 ++++++------------- core/libs/prx/libmdbg_syscore/Export.cpp | 6 ++-- docs/dev/debt/libSceComposite.md | 2 +- docs/dev/debt/libSceFsInternalForVsh.md | 2 +- docs/dev/debt/libSceHttpCache.md | 4 +-- docs/dev/debt/libScePlayReady.md | 2 +- docs/dev/debt/libSceSsl.md | 2 +- docs/dev/debt/libSceVisionManager.md | 2 +- docs/dev/debt/libmdbg_syscore.md | 2 +- 15 files changed, 28 insertions(+), 57 deletions(-) diff --git a/core/libs/prx/libSceComposite/Export.cpp b/core/libs/prx/libSceComposite/Export.cpp index a2e79ae3ed..96949e8e2f 100644 --- a/core/libs/prx/libSceComposite/Export.cpp +++ b/core/libs/prx/libSceComposite/Export.cpp @@ -27,13 +27,11 @@ int APS5_VABI sceCompositorCommandGpuPerfEnd() { } int APS5_VABI sceCompositorCreateIndirectRenderTarget() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80d40006); } int APS5_VABI sceCompositorDeleteIndirectRenderTarget() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80d40006); } int APS5_VABI sceCompositorFlush() { @@ -262,17 +260,14 @@ int APS5_VABI sceCompositorSetGnmContextCommand() { } int APS5_VABI sceCompositorSetIndirectCanvasCommand() { - NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceCompositorSetIndirectCompositionCommand() { - NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceCompositorSetIndirectRenderTargetConfigCommand() { - NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceFsInternalForVsh/Export.cpp b/core/libs/prx/libSceFsInternalForVsh/Export.cpp index 75159006f0..e98aa4a828 100644 --- a/core/libs/prx/libSceFsInternalForVsh/Export.cpp +++ b/core/libs/prx/libSceFsInternalForVsh/Export.cpp @@ -212,13 +212,11 @@ int APS5_VABI sceFsDrfsResumeSocket() { } int APS5_VABI sceFsErriCtl() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x8002002d); } int APS5_VABI sceFsErriGetState() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x8002002d); } int APS5_VABI sceFsExfatMkfsExternalHdd() { diff --git a/core/libs/prx/libSceHttpCache/Export.cpp b/core/libs/prx/libSceHttpCache/Export.cpp index d3bdbf753f..b7f0f3ef35 100644 --- a/core/libs/prx/libSceHttpCache/Export.cpp +++ b/core/libs/prx/libSceHttpCache/Export.cpp @@ -136,7 +136,6 @@ int APS5_VABI sceHttpCacheClearCacheInfo() { } int APS5_VABI sceHttpCacheDebugSetUrl() { - NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libScePlayReady/Export.cpp b/core/libs/prx/libScePlayReady/Export.cpp index fa7958b257..4c7e5fe395 100644 --- a/core/libs/prx/libScePlayReady/Export.cpp +++ b/core/libs/prx/libScePlayReady/Export.cpp @@ -217,8 +217,7 @@ int APS5_VABI scePlayReadyMemFree() { } int APS5_VABI scePlayReadyMemGetStats() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80004001); } int APS5_VABI scePlayReadyMemRealloc() { diff --git a/core/libs/prx/libSceSdma/Export.cpp b/core/libs/prx/libSceSdma/Export.cpp index 1e67f3a5bb..45a5fcc447 100644 --- a/core/libs/prx/libSceSdma/Export.cpp +++ b/core/libs/prx/libSceSdma/Export.cpp @@ -93,7 +93,6 @@ int APS5_VABI sceSdmaFenceNonBlocking(void) { } int APS5_VABI sceSdmaFinalize(void) { - NotImplemented_nid_no_patch(__func__); return 0; } @@ -103,12 +102,10 @@ int APS5_VABI sceSdmaFlushAll(void) { } int APS5_VABI sceSdmaFlushAllWithEvent(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x809900c9); } int APS5_VABI sceSdmaInitialize(void) { - NotImplemented_nid_no_patch(__func__); return 0; } @@ -123,8 +120,7 @@ int APS5_VABI sceSdmaOpen(void) { } int APS5_VABI sceSdmaQueryWorkingBufferSize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return 0x8000; } int APS5_VABI sceSdmaTryFlush(void) { @@ -133,8 +129,7 @@ int APS5_VABI sceSdmaTryFlush(void) { } int APS5_VABI sceSdmaWaitForEvent(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x809900c9); } } diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index 426b2658c5..2daacaffb0 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -1261,7 +1261,6 @@ int APS5_VABI CRYPTO_initAsymmetricKey() { } int APS5_VABI CRYPTO_uninitAsymmetricKey() { - NotImplemented_nid_no_patch(__func__); return 0; } @@ -1271,7 +1270,6 @@ int APS5_VABI RSA_verifySignature() { } int APS5_VABI VLONG_freeVlongQueue() { - NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceVisionManager/Export.cpp b/core/libs/prx/libSceVisionManager/Export.cpp index de5e1986f2..e329980232 100644 --- a/core/libs/prx/libSceVisionManager/Export.cpp +++ b/core/libs/prx/libSceVisionManager/Export.cpp @@ -17,8 +17,7 @@ int APS5_VABI sceVisionManagerCancelRequest() { } int APS5_VABI sceVisionManagerClose() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80e70004); } int APS5_VABI sceVisionManagerFinalize() { @@ -107,23 +106,19 @@ int APS5_VABI sceVisionManagerGetResultOfFindPadCorrespondRegion() { } int APS5_VABI sceVisionManagerGetResultOfFindUser() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80e70004); } int APS5_VABI sceVisionManagerGetResultOfFindUserInRegion() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80e70004); } int APS5_VABI sceVisionManagerGetResultOfFindUserWithoutIdentify() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80e70004); } int APS5_VABI sceVisionManagerGetResultOfRegisterUserInRegion() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80e70004); } int APS5_VABI sceVisionManagerGetStoreCameraImgYPtr() { @@ -142,13 +137,11 @@ int APS5_VABI sceVisionManagerInitialize() { } int APS5_VABI sceVisionManagerLoadSettings() { - NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceVisionManagerOpen() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80e70004); } int APS5_VABI sceVisionManagerRegistryClear() { @@ -192,18 +185,15 @@ int APS5_VABI sceVisionManagerRequestFindPadCorrespondRegion() { } int APS5_VABI sceVisionManagerRequestFindUser() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80e70004); } int APS5_VABI sceVisionManagerRequestFindUserInRegion() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80e70004); } int APS5_VABI sceVisionManagerRequestFindUserWithoutIdentify() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80e70004); } int APS5_VABI sceVisionManagerRequestHeadDetection() { @@ -232,8 +222,7 @@ int APS5_VABI sceVisionManagerRequestRecogUtility() { } int APS5_VABI sceVisionManagerRequestRegisterUserInRegion() { - NotImplemented_nid_no_patch(__func__); - return 0; + return static_cast(0x80e70004); } int APS5_VABI sceVisionManagerRequestStoreCameraImgY() { diff --git a/core/libs/prx/libmdbg_syscore/Export.cpp b/core/libs/prx/libmdbg_syscore/Export.cpp index 77409149dd..98fb4bdf87 100644 --- a/core/libs/prx/libmdbg_syscore/Export.cpp +++ b/core/libs/prx/libmdbg_syscore/Export.cpp @@ -227,8 +227,7 @@ int APS5_VABI sceDebugGetProcessCoredumpHandlerResult() { } int APS5_VABI sceDebugGetProcessEventCntlFlag() { - NotImplemented_nid_no_patch(__func__); - return 0; + return 1; } int APS5_VABI sceDebugGetProcessInfo() { @@ -652,8 +651,7 @@ int APS5_VABI sceDebugResumeThread() { } int APS5_VABI sceDebugSetProcessEventCntlFlag() { - NotImplemented_nid_no_patch(__func__); - return 0; + return 1; } int APS5_VABI sceDebugSetStepThread() { diff --git a/docs/dev/debt/libSceComposite.md b/docs/dev/debt/libSceComposite.md index 45ba88b721..36d78197ce 100644 --- a/docs/dev/debt/libSceComposite.md +++ b/docs/dev/debt/libSceComposite.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceCompositorAllocateIndex, sceCompositorCheckCrash, sceCompositorCommandGpuPerfBegin, sceCompositorCommandGpuPerfEnd, sceCompositorCreateIndirectRenderTarget, sceCompositorDeleteIndirectRenderTarget, sceCompositorFlush, sceCompositorFlushWithRepeat, sceCompositorGetAnotherProcessSystemAddress, sceCompositorGetAnotherProcessSystemSize, sceCompositorGetAnotherProcessVideoAddress, sceCompositorGetAnotherProcessVideoSize, sceCompositorGetCanvasHandle, sceCompositorGetClientList, sceCompositorGetDmemOffset, sceCompositorGetDummyAddress, sceCompositorGetDummySize, sceCompositorGetRemainedCommandNum, sceCompositorGetRenderTargetResolution, sceCompositorGetSystemAddress, sceCompositorGetSystemSize, sceCompositorGetVideoAddress, sceCompositorGetVideoSize, sceCompositorHandleProcessEvents, sceCompositorInit, sceCompositorInitWithProcessOrder, sceCompositorInsertThreadTraceMarker, sceCompositorIsDebugCaptureEnabled, sceCompositorIsReady4K, sceCompositorLockCommandBuffer, sceCompositorMakeCanvasHandle, sceCompositorMapAnotherProcess, sceCompositorMemoryPoolDecommit, sceCompositorPrivacyMaskPushArea, sceCompositorQuit, sceCompositorReleaseCommandBuffer, sceCompositorReleaseIndex, sceCompositorReleaseLocalStall, sceCompositorReserveCommandBuffer, sceCompositorResetZoomCommand, sceCompositorSetAgcCompositeCanvasCommand, sceCompositorSetAgcContextCommand, sceCompositorSetCompositeCanvasCommandInC, sceCompositorSetCursorImageAddress, sceCompositorSetDebugPositionCommand, sceCompositorSetEndOfFrameCommand, sceCompositorSetEventCommand, sceCompositorSetFilterType, sceCompositorSetFlipCommand2, sceCompositorSetGameSufaceControlCommand, sceCompositorSetGnmContextCommand, sceCompositorSetIndirectCanvasCommand, sceCompositorSetIndirectCompositionCommand, sceCompositorSetIndirectRenderTargetConfigCommand, sceCompositorSetInvisibleCanvasCommand, sceCompositorSetKeepDisplayBufferCommand, sceCompositorSetLocalStallCommand, sceCompositorSetMemoryCommand, sceCompositorSetPatchCommand, sceCompositorSetPostEventCommand, sceCompositorSetRepeatCommand, sceCompositorSetResolutionCommand, sceCompositorSetScalingLayoutInfo, sceCompositorSetSeparateContextCommand, sceCompositorSetSystemConfigWithMask, sceCompositorSetVideoOutMode, sceCompositorSetVirtualCanvasAgcContextCommand, sceCompositorSetVirtualCanvasCommandFooter, sceCompositorSetVirtualCanvasPatchCommand, sceCompositorSetVrMode, sceCompositorSetZoomCommand, sceCompositorSystemConfig, sceCompositorWaitEndOfRendering, sceCompositorWaitEvent, sceCompositorWaitPostEvent, sceCompsoitorGetGpuClock, sceCompsoitorGetProcessRenderingTime and sceCompsoitorGetRenderingTime](../../../core/libs/prx/libSceComposite/Export.cpp) (libSceComposite) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceComposite.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceCompositorAllocateIndex, sceCompositorCheckCrash, sceCompositorCommandGpuPerfBegin, sceCompositorCommandGpuPerfEnd, sceCompositorFlush, sceCompositorFlushWithRepeat, sceCompositorGetAnotherProcessSystemAddress, sceCompositorGetAnotherProcessSystemSize, sceCompositorGetAnotherProcessVideoAddress, sceCompositorGetAnotherProcessVideoSize, sceCompositorGetCanvasHandle, sceCompositorGetClientList, sceCompositorGetDmemOffset, sceCompositorGetDummyAddress, sceCompositorGetDummySize, sceCompositorGetRemainedCommandNum, sceCompositorGetRenderTargetResolution, sceCompositorGetSystemAddress, sceCompositorGetSystemSize, sceCompositorGetVideoAddress, sceCompositorGetVideoSize, sceCompositorHandleProcessEvents, sceCompositorInit, sceCompositorInitWithProcessOrder, sceCompositorInsertThreadTraceMarker, sceCompositorIsDebugCaptureEnabled, sceCompositorIsReady4K, sceCompositorLockCommandBuffer, sceCompositorMakeCanvasHandle, sceCompositorMapAnotherProcess, sceCompositorMemoryPoolDecommit, sceCompositorPrivacyMaskPushArea, sceCompositorQuit, sceCompositorReleaseCommandBuffer, sceCompositorReleaseIndex, sceCompositorReleaseLocalStall, sceCompositorReserveCommandBuffer, sceCompositorResetZoomCommand, sceCompositorSetAgcCompositeCanvasCommand, sceCompositorSetAgcContextCommand, sceCompositorSetCompositeCanvasCommandInC, sceCompositorSetCursorImageAddress, sceCompositorSetDebugPositionCommand, sceCompositorSetEndOfFrameCommand, sceCompositorSetEventCommand, sceCompositorSetFilterType, sceCompositorSetFlipCommand2, sceCompositorSetGameSufaceControlCommand, sceCompositorSetGnmContextCommand, sceCompositorSetInvisibleCanvasCommand, sceCompositorSetKeepDisplayBufferCommand, sceCompositorSetLocalStallCommand, sceCompositorSetMemoryCommand, sceCompositorSetPatchCommand, sceCompositorSetPostEventCommand, sceCompositorSetRepeatCommand, sceCompositorSetResolutionCommand, sceCompositorSetScalingLayoutInfo, sceCompositorSetSeparateContextCommand, sceCompositorSetSystemConfigWithMask, sceCompositorSetVideoOutMode, sceCompositorSetVirtualCanvasAgcContextCommand, sceCompositorSetVirtualCanvasCommandFooter, sceCompositorSetVirtualCanvasPatchCommand, sceCompositorSetVrMode, sceCompositorSetZoomCommand, sceCompositorSystemConfig, sceCompositorWaitEndOfRendering, sceCompositorWaitEvent, sceCompositorWaitPostEvent, sceCompsoitorGetGpuClock, sceCompsoitorGetProcessRenderingTime and sceCompsoitorGetRenderingTime](../../../core/libs/prx/libSceComposite/Export.cpp) (libSceComposite) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceComposite.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceFsInternalForVsh.md b/docs/dev/debt/libSceFsInternalForVsh.md index 92591afa7b..4e242ae883 100644 --- a/docs/dev/debt/libSceFsInternalForVsh.md +++ b/docs/dev/debt/libSceFsInternalForVsh.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceBdSchedCancelBackgroundCopyRequest, sceBdSchedCancelPrioritizedBackgroundCopyRequest, sceBdSchedConfigure, sceBdSchedConfigure2, sceBdSchedDeconfigure, sceBdSchedGetBackgroundCopyRequest, sceBdSchedGetExtentMap, sceBdSchedGetPrioritizedBackgroundCopyRequest, sceBdSchedGetState, sceBdSchedSetBackgroundCopyRequest, sceBdSchedSetDiscIdentity, sceBdSchedSetPrioritizedBackgroundCopyRequest, sceBdSchedStartGame, sceBdSchedStartRipping, sceBdSchedStopScheduling, sceFsBfsCheckCleanFlag, sceFsBindApplication, sceFsBindPackages, sceFsCheckFormat, sceFsCheckSingleUser, sceFsCreateExfatPartition, sceFsCreatePfsSaveDataImage, sceFsCreatePfsTrophyDataImage, sceFsCreatePprPfsTrophyDataImage, sceFsDeviceAlignedPread, sceFsDeviceAlignedPwrite, sceFsDevpfsA53IocGetBuildMetadataStatus, sceFsDevpfsCancelWaitEvent, sceFsDevpfsCreateForVSH, sceFsDevpfsDeleteForVSH, sceFsDevpfsDeregisterEvent, sceFsDevpfsEventComplete, sceFsDevpfsEventResponseComplete, sceFsDevpfsFetchMetaEntry, sceFsDevpfsFsck, sceFsDevpfsInitFetchMetaEntryCtx, sceFsDevpfsMountForFSMP, sceFsDevpfsRegisterEvent, sceFsDevpfsWaitEvent, sceFsDrfsRegisterSocket, sceFsDrfsResumeSocket, sceFsErriCtl, sceFsErriGetState, sceFsExfatMkfsExternalHdd, sceFsExfatfsMkfs, sceFsExtUSBSchedDestroy, sceFsExtUSBSchedInsert, sceFsExtUSBSchedInsertForHDD, sceFsExternalStorageDeconfigure, sceFsExternalStorageGetRawDevice, sceFsExternalStorageInitialize, sceFsExternalStorageVerifyMetadata, sceFsGetAttribute, sceFsGetClusterStatistics, sceFsGetDeviceSectorsize, sceFsGetFsMetadata, sceFsGetFsMetadataWithProgress, sceFsGetSmrInfo, sceFsGschedCtl, sceFsISSchedConfigCurrentThread, sceFsISSchedConfigDescriptor, sceFsISSchedConfigThread, sceFsISSchedMapStat, sceFsISSchedResultToPriority, sceFsISSchedResultToSlot, sceFsISSchedStatCurrentThread, sceFsISSchedStatDescriptor, sceFsISSchedStatThread, sceFsISSchedUnmapStat, sceFsInitCreatePfsSaveDataOpt, sceFsInitCreatePfsTrophyDataOpt, sceFsInitMountDownloadDataOpt, sceFsInitMountGamePkgOpt, sceFsInitMountLwfsCompatOpt, sceFsInitMountLwfsOpt, sceFsInitMountPprPkgOpt, sceFsInitMountSaveDataOpt, sceFsInitMountTrophyDataOpt, sceFsInitUmountDownloadDataOpt, sceFsInitUmountGamePkgOpt, sceFsInitUmountLwfsCompatOpt, sceFsInitUmountLwfsOpt, sceFsInitUmountPprPkgOpt, sceFsInitUmountSaveDataOpt, sceFsInitUmountTrophyDataOpt, sceFsInitWorkspaceFileMetadataParam, sceFsLvdAttach, sceFsLvdAttach2, sceFsLvdAttachPartiallyCryptedDownloadData, sceFsLvdAttachSingleDefaultImage, sceFsLvdAttachSingleImage, sceFsLvdDetach, sceFsLvdDisableLayer, sceFsLvdEnableLayer, sceFsLvdSetDiscIdentity, sceFsLvdWaitResumed, sceFsMlfsMount, sceFsMlfsMountWithFlags, sceFsMlfsUmount, sceFsMountDownloadData, sceFsMountGamePkg, sceFsMountLwfs, sceFsMountLwfsCompat, sceFsMountPartiallyCryptedDownloadData, sceFsMountPprPkg, sceFsMountSaveData, sceFsMountTrophyData, sceFsNsfsAddOverlayToNamespace, sceFsNsfsMountWithFlags, sceFsNsfsSetGlobalExclude, sceFsNsfsUmount, sceFsPartitionTrim, sceFsReserveCluster, sceFsSetAttribute, sceFsSetClusterCacheSize, sceFsSetFileReservedTotal, sceFsSetSystemReserveSize, sceFsSetWorkspaceFileHash, sceFsStatWorkspace, sceFsSysStorageGetStatus, sceFsTransactionCancel, sceFsTransactionSaveDataInitMountOpt, sceFsTransactionSaveDataMount, sceFsTransactionStart, sceFsTrophyImageError, sceFsUfsAllocateAdditionalContents, sceFsUfsAllocateAligned, sceFsUfsAllocateDownloadData, sceFsUfsAllocateGameImage, sceFsUfsAllocatePatchImage, sceFsUfsAllocateSaveData, sceFsUfsAllocateTrophyData, sceFsUfsCheckCleanFlag, sceFsUfsCheckFixedCylinderGroupSize, sceFsUfsCollectTelemetry, sceFsUfsCollectTelemetry2, sceFsUfsDirectWrite, sceFsUfsEstimateZoneCapacityForUser, sceFsUfsExtendFile, sceFsUfsFsck, sceFsUfsFsckWithMM, sceFsUfsGetExtentMap, sceFsUfsGrowLwfs, sceFsUfsGrowfs, sceFsUfsHideBlocksFromUser, sceFsUfsInitalizeSparseFile, sceFsUfsLargewriteFsck, sceFsUfsLargewriteFsckWithMM, sceFsUfsMkLwfs, sceFsUfsMkfs, sceFsUfsMkfsForExternalHDD, sceFsUfsMkfsForSystem, sceFsUfsMkfsWithFixedCylinderGroupSize, sceFsUfsReallocateSparseFile, sceFsUfsRecryptBlocks, sceFsUfsRecryptGetState, sceFsUfsRecryptIsUnfinished, sceFsUfsRecryptSyncMetadata, sceFsUfsSeekHoleInFile, sceFsUfsStopOperation, sceFsUmountDownloadData, sceFsUmountGamePkg, sceFsUmountLwfs, sceFsUmountLwfsCompat, sceFsUmountSaveData, sceFsUmountTrophyData, sceFsUmountWorkspace, sceFsUnbindApplication, sceFsUnbindPackages, sceFsWsCreate, sceFsWsDelete, sceFsWsFetchFileMetadata, sceFsWsFsck, sceFsWsGetStatus, sceFsWsMount and sceFsWsUmount](../../../core/libs/prx/libSceFsInternalForVsh/Export.cpp) (libSceFsInternalForVsh) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFsInternalForVsh.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceBdSchedCancelBackgroundCopyRequest, sceBdSchedCancelPrioritizedBackgroundCopyRequest, sceBdSchedConfigure, sceBdSchedConfigure2, sceBdSchedDeconfigure, sceBdSchedGetBackgroundCopyRequest, sceBdSchedGetExtentMap, sceBdSchedGetPrioritizedBackgroundCopyRequest, sceBdSchedGetState, sceBdSchedSetBackgroundCopyRequest, sceBdSchedSetDiscIdentity, sceBdSchedSetPrioritizedBackgroundCopyRequest, sceBdSchedStartGame, sceBdSchedStartRipping, sceBdSchedStopScheduling, sceFsBfsCheckCleanFlag, sceFsBindApplication, sceFsBindPackages, sceFsCheckFormat, sceFsCheckSingleUser, sceFsCreateExfatPartition, sceFsCreatePfsSaveDataImage, sceFsCreatePfsTrophyDataImage, sceFsCreatePprPfsTrophyDataImage, sceFsDeviceAlignedPread, sceFsDeviceAlignedPwrite, sceFsDevpfsA53IocGetBuildMetadataStatus, sceFsDevpfsCancelWaitEvent, sceFsDevpfsCreateForVSH, sceFsDevpfsDeleteForVSH, sceFsDevpfsDeregisterEvent, sceFsDevpfsEventComplete, sceFsDevpfsEventResponseComplete, sceFsDevpfsFetchMetaEntry, sceFsDevpfsFsck, sceFsDevpfsInitFetchMetaEntryCtx, sceFsDevpfsMountForFSMP, sceFsDevpfsRegisterEvent, sceFsDevpfsWaitEvent, sceFsDrfsRegisterSocket, sceFsDrfsResumeSocket, sceFsExfatMkfsExternalHdd, sceFsExfatfsMkfs, sceFsExtUSBSchedDestroy, sceFsExtUSBSchedInsert, sceFsExtUSBSchedInsertForHDD, sceFsExternalStorageDeconfigure, sceFsExternalStorageGetRawDevice, sceFsExternalStorageInitialize, sceFsExternalStorageVerifyMetadata, sceFsGetAttribute, sceFsGetClusterStatistics, sceFsGetDeviceSectorsize, sceFsGetFsMetadata, sceFsGetFsMetadataWithProgress, sceFsGetSmrInfo, sceFsGschedCtl, sceFsISSchedConfigCurrentThread, sceFsISSchedConfigDescriptor, sceFsISSchedConfigThread, sceFsISSchedMapStat, sceFsISSchedResultToPriority, sceFsISSchedResultToSlot, sceFsISSchedStatCurrentThread, sceFsISSchedStatDescriptor, sceFsISSchedStatThread, sceFsISSchedUnmapStat, sceFsInitCreatePfsSaveDataOpt, sceFsInitCreatePfsTrophyDataOpt, sceFsInitMountDownloadDataOpt, sceFsInitMountGamePkgOpt, sceFsInitMountLwfsCompatOpt, sceFsInitMountLwfsOpt, sceFsInitMountPprPkgOpt, sceFsInitMountSaveDataOpt, sceFsInitMountTrophyDataOpt, sceFsInitUmountDownloadDataOpt, sceFsInitUmountGamePkgOpt, sceFsInitUmountLwfsCompatOpt, sceFsInitUmountLwfsOpt, sceFsInitUmountPprPkgOpt, sceFsInitUmountSaveDataOpt, sceFsInitUmountTrophyDataOpt, sceFsInitWorkspaceFileMetadataParam, sceFsLvdAttach, sceFsLvdAttach2, sceFsLvdAttachPartiallyCryptedDownloadData, sceFsLvdAttachSingleDefaultImage, sceFsLvdAttachSingleImage, sceFsLvdDetach, sceFsLvdDisableLayer, sceFsLvdEnableLayer, sceFsLvdSetDiscIdentity, sceFsLvdWaitResumed, sceFsMlfsMount, sceFsMlfsMountWithFlags, sceFsMlfsUmount, sceFsMountDownloadData, sceFsMountGamePkg, sceFsMountLwfs, sceFsMountLwfsCompat, sceFsMountPartiallyCryptedDownloadData, sceFsMountPprPkg, sceFsMountSaveData, sceFsMountTrophyData, sceFsNsfsAddOverlayToNamespace, sceFsNsfsMountWithFlags, sceFsNsfsSetGlobalExclude, sceFsNsfsUmount, sceFsPartitionTrim, sceFsReserveCluster, sceFsSetAttribute, sceFsSetClusterCacheSize, sceFsSetFileReservedTotal, sceFsSetSystemReserveSize, sceFsSetWorkspaceFileHash, sceFsStatWorkspace, sceFsSysStorageGetStatus, sceFsTransactionCancel, sceFsTransactionSaveDataInitMountOpt, sceFsTransactionSaveDataMount, sceFsTransactionStart, sceFsTrophyImageError, sceFsUfsAllocateAdditionalContents, sceFsUfsAllocateAligned, sceFsUfsAllocateDownloadData, sceFsUfsAllocateGameImage, sceFsUfsAllocatePatchImage, sceFsUfsAllocateSaveData, sceFsUfsAllocateTrophyData, sceFsUfsCheckCleanFlag, sceFsUfsCheckFixedCylinderGroupSize, sceFsUfsCollectTelemetry, sceFsUfsCollectTelemetry2, sceFsUfsDirectWrite, sceFsUfsEstimateZoneCapacityForUser, sceFsUfsExtendFile, sceFsUfsFsck, sceFsUfsFsckWithMM, sceFsUfsGetExtentMap, sceFsUfsGrowLwfs, sceFsUfsGrowfs, sceFsUfsHideBlocksFromUser, sceFsUfsInitalizeSparseFile, sceFsUfsLargewriteFsck, sceFsUfsLargewriteFsckWithMM, sceFsUfsMkLwfs, sceFsUfsMkfs, sceFsUfsMkfsForExternalHDD, sceFsUfsMkfsForSystem, sceFsUfsMkfsWithFixedCylinderGroupSize, sceFsUfsReallocateSparseFile, sceFsUfsRecryptBlocks, sceFsUfsRecryptGetState, sceFsUfsRecryptIsUnfinished, sceFsUfsRecryptSyncMetadata, sceFsUfsSeekHoleInFile, sceFsUfsStopOperation, sceFsUmountDownloadData, sceFsUmountGamePkg, sceFsUmountLwfs, sceFsUmountLwfsCompat, sceFsUmountSaveData, sceFsUmountTrophyData, sceFsUmountWorkspace, sceFsUnbindApplication, sceFsUnbindPackages, sceFsWsCreate, sceFsWsDelete, sceFsWsFetchFileMetadata, sceFsWsFsck, sceFsWsGetStatus, sceFsWsMount and sceFsWsUmount](../../../core/libs/prx/libSceFsInternalForVsh/Export.cpp) (libSceFsInternalForVsh) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFsInternalForVsh.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceHttpCache.md b/docs/dev/debt/libSceHttpCache.md index a2d7b748a6..265fb144aa 100644 --- a/docs/dev/debt/libSceHttpCache.md +++ b/docs/dev/debt/libSceHttpCache.md @@ -2,5 +2,5 @@ ### Unknown function info -- [sceHttpCacheSystemSendStatistics and sceHttpCacheSystemTerm](../../../core/libs/prx/libSceHttpCache/Export.cpp): the supplied implementations return zero without reading arguments. No callers have yet established whether their public prototypes include unused arguments. -- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceHttpCacheClearAllUserTags`](../../../core/libs/prx/libSceHttpCache/Export.cpp), [`sceHttpCacheClearCacheInfo`](../../../core/libs/prx/libSceHttpCache/Export.cpp), [`sceHttpCacheDebugSetUrl`](../../../core/libs/prx/libSceHttpCache/Export.cpp), [`sceHttpCacheRegisterCacheInfoUrl`](../../../core/libs/prx/libSceHttpCache/Export.cpp), [`sceHttpCacheRemoveTag`](../../../core/libs/prx/libSceHttpCache/Export.cpp). +- [sceHttpCacheDebugSetUrl, sceHttpCacheSystemSendStatistics and sceHttpCacheSystemTerm](../../../core/libs/prx/libSceHttpCache/Export.cpp): the supplied implementations return zero without reading arguments. No callers have yet established whether their public prototypes include unused arguments. +- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceHttpCacheClearAllUserTags`](../../../core/libs/prx/libSceHttpCache/Export.cpp), [`sceHttpCacheClearCacheInfo`](../../../core/libs/prx/libSceHttpCache/Export.cpp), [`sceHttpCacheRegisterCacheInfoUrl`](../../../core/libs/prx/libSceHttpCache/Export.cpp), [`sceHttpCacheRemoveTag`](../../../core/libs/prx/libSceHttpCache/Export.cpp). diff --git a/docs/dev/debt/libScePlayReady.md b/docs/dev/debt/libScePlayReady.md index f3b7a6a60c..c7677e9529 100644 --- a/docs/dev/debt/libScePlayReady.md +++ b/docs/dev/debt/libScePlayReady.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceBufferAlloc, sceBufferDup, sceBufferFree, sceBufferInit, sceBufferRealloc, scePlayReadyApiInitializeInitParams, scePlayReadyApiIsStarted, scePlayReadyApiStart, scePlayReadyApiStop, scePlayReadyApiVersion, scePlayReadyApiVersionString, scePlayReadyBufferFree, scePlayReadyBufferInit, scePlayReadyBufferMalloc, scePlayReadyBufferRealloc, scePlayReadyContentGetProperty, scePlayReadyContentSetProperty, scePlayReadyDomainCertEnumFini, scePlayReadyDomainCertEnumInit, scePlayReadyDomainCertEnumNext, scePlayReadyDomainCertFind, scePlayReadyDomainJoinGenerateChallenge, scePlayReadyDomainJoinProcessResponse, scePlayReadyDomainLeaveGenerateChallenge, scePlayReadyDomainLeaveProcessResponse, scePlayReadyEnvelopeClose, scePlayReadyEnvelopeGetSize, scePlayReadyEnvelopeInitializeRead, scePlayReadyEnvelopeOpen, scePlayReadyEnvelopeRead, scePlayReadyEnvelopeSeek, scePlayReadyFinalize, scePlayReadyFreeDecryptContext, scePlayReadyInitialize, scePlayReadyInitializeInitParams, scePlayReadyLicenseAcqGenerateAck, scePlayReadyLicenseAcqGenerateChallenge, scePlayReadyLicenseAcqGetAdditionalResponseData, scePlayReadyLicenseAcqProcessAckResponse, scePlayReadyLicenseAcqProcessResponse, scePlayReadyMemAlloc, scePlayReadyMemFree, scePlayReadyMemGetStats, scePlayReadyMemRealloc, scePlayReadyNewDecryptContext, scePlayReadyReaderBind, scePlayReadyReaderCommit, scePlayReadyReaderDecrypt, scePlayReadyReset and scePlayReadyStoreCleanup](../../../core/libs/prx/libScePlayReady/Export.cpp) (libScePlayReady) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePlayReady.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceBufferAlloc, sceBufferDup, sceBufferFree, sceBufferInit, sceBufferRealloc, scePlayReadyApiInitializeInitParams, scePlayReadyApiIsStarted, scePlayReadyApiStart, scePlayReadyApiStop, scePlayReadyApiVersion, scePlayReadyApiVersionString, scePlayReadyBufferFree, scePlayReadyBufferInit, scePlayReadyBufferMalloc, scePlayReadyBufferRealloc, scePlayReadyContentGetProperty, scePlayReadyContentSetProperty, scePlayReadyDomainCertEnumFini, scePlayReadyDomainCertEnumInit, scePlayReadyDomainCertEnumNext, scePlayReadyDomainCertFind, scePlayReadyDomainJoinGenerateChallenge, scePlayReadyDomainJoinProcessResponse, scePlayReadyDomainLeaveGenerateChallenge, scePlayReadyDomainLeaveProcessResponse, scePlayReadyEnvelopeClose, scePlayReadyEnvelopeGetSize, scePlayReadyEnvelopeInitializeRead, scePlayReadyEnvelopeOpen, scePlayReadyEnvelopeRead, scePlayReadyEnvelopeSeek, scePlayReadyFinalize, scePlayReadyFreeDecryptContext, scePlayReadyInitialize, scePlayReadyInitializeInitParams, scePlayReadyLicenseAcqGenerateAck, scePlayReadyLicenseAcqGenerateChallenge, scePlayReadyLicenseAcqGetAdditionalResponseData, scePlayReadyLicenseAcqProcessAckResponse, scePlayReadyLicenseAcqProcessResponse, scePlayReadyMemAlloc, scePlayReadyMemFree, scePlayReadyMemRealloc, scePlayReadyNewDecryptContext, scePlayReadyReaderBind, scePlayReadyReaderCommit, scePlayReadyReaderDecrypt, scePlayReadyReset and scePlayReadyStoreCleanup](../../../core/libs/prx/libScePlayReady/Export.cpp) (libScePlayReady) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePlayReady.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index f883857ecc..03b0372d75 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -2,7 +2,7 @@ ### Unknown function info -- [CA_MGMT_extractKeyBlobEx, CA_MGMT_extractPublicKeyInfo, CA_MGMT_freeKeyBlob, CRYPTO_initAsymmetricKey, CRYPTO_uninitAsymmetricKey, RSA_verifySignature, VLONG_freeVlongQueue, sceSslCheckRecvPending, sceSslCreateSslConnection and sceSslDeleteSslConnection](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - the list is the intersection of the export table of the console's libSceSsl (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo. Names and NIDs appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSsl.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); no credible prototypes were found. They are declared without parameters solely to resolve known imports, not as an ABI claim; callers must not call them until signatures are verified. +- [CA_MGMT_extractKeyBlobEx, CA_MGMT_extractPublicKeyInfo, CA_MGMT_freeKeyBlob, CRYPTO_initAsymmetricKey, RSA_verifySignature, sceSslCheckRecvPending, sceSslCreateSslConnection and sceSslDeleteSslConnection](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - the list is the intersection of the export table of the console's libSceSsl (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo. Names and NIDs appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSsl.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); no credible prototypes were found. They are declared without parameters solely to resolve known imports, not as an ABI claim; callers must not call them until signatures are verified. - [sceSslDisableOption, sceSslDisableOptionInternal, sceSslDisableOptionInternalInsecure, sceSslEnableOption, sceSslEnableOptionInternal, sceSslGetNanoSSLModuleId, sceSslGetPeerCert, sceSslGetSslError, sceSslLoadRootCACert and sceSslSetSslVersion](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - the list is the intersection of the export table of the console's libSceSsl (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo. Names and NIDs appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSsl.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); no credible prototypes were found. They are declared without parameters solely to resolve known imports, not as an ABI claim; callers must not call them until signatures are verified. - [sceSslReuseConnection](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - its name and NID appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSsl.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); its parameter shape follows [SharpProspero's pinned binding](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs#L2231-L2235). No console observation or reliable implementation source for session reuse was found. diff --git a/docs/dev/debt/libSceVisionManager.md b/docs/dev/debt/libSceVisionManager.md index 6e8c187895..f8f41d421e 100644 --- a/docs/dev/debt/libSceVisionManager.md +++ b/docs/dev/debt/libSceVisionManager.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceVisionManagerApplyCalibrateHmdResult, sceVisionManagerCancelRequest, sceVisionManagerClose, sceVisionManagerFinalize, sceVisionManagerGenerateRegisterUserData, sceVisionManagerGenerateRegisterUserDataFinalize, sceVisionManagerGenerateRegisterUserDataInitialize, sceVisionManagerGetCalibrateTrackingLedInfo, sceVisionManagerGetCalibrateTrackingLedNecessity, sceVisionManagerGetHitCountOfRegisterUserData, sceVisionManagerGetLibraryVersion, sceVisionManagerGetNumOfRegisterUserDataSlot, sceVisionManagerGetRegisterUserDataSize, sceVisionManagerGetRegisterUserDataVersion, sceVisionManagerGetResultOfCalibrateHmd, sceVisionManagerGetResultOfCalibrateStereoCamera, sceVisionManagerGetResultOfCalibrateTrackingLed, sceVisionManagerGetResultOfFindFace, sceVisionManagerGetResultOfFindPad, sceVisionManagerGetResultOfFindPadCorrespondRegion, sceVisionManagerGetResultOfFindUser, sceVisionManagerGetResultOfFindUserInRegion, sceVisionManagerGetResultOfFindUserWithoutIdentify, sceVisionManagerGetResultOfRegisterUserInRegion, sceVisionManagerGetStoreCameraImgYPtr, sceVisionManagerGetWorkingMemorySize, sceVisionManagerInitialize, sceVisionManagerLoadSettings, sceVisionManagerOpen, sceVisionManagerRegistryClear, sceVisionManagerRequestCalibrateHmd, sceVisionManagerRequestCalibrateStereoCamera, sceVisionManagerRequestCalibrateTrackingLed, sceVisionManagerRequestClose, sceVisionManagerRequestFindFace, sceVisionManagerRequestFindPad, sceVisionManagerRequestFindPadCorrespondRegion, sceVisionManagerRequestFindUser, sceVisionManagerRequestFindUserInRegion, sceVisionManagerRequestFindUserWithoutIdentify, sceVisionManagerRequestHeadDetection, sceVisionManagerRequestInitializeRegisterUserData, sceVisionManagerRequestOpen, sceVisionManagerRequestPadTracking, sceVisionManagerRequestRecogUtility, sceVisionManagerRequestRegisterUserInRegion, sceVisionManagerRequestStoreCameraImgY, sceVisionManagerSetCallbackForUpdateCalibrateHmdResult, sceVisionManagerSetCallbackForUpdateCalibrateStereoCameraResult, sceVisionManagerSetCallbackForUpdateCalibrateTrackingLedResult, sceVisionManagerSetCallbackForUpdateCameraFrame, sceVisionManagerSetCallbackForUpdateCtrlCore, sceVisionManagerSetCallbackForUpdateRecog, sceVisionManagerSetCallbackForUpdateRunningStatus, sceVisionManagerSetCallbackForUpdateStoreFrame and sceVisionManagerWaitResponse](../../../core/libs/prx/libSceVisionManager/Export.cpp) (libSceVisionManager) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVisionManager.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceVisionManagerApplyCalibrateHmdResult, sceVisionManagerCancelRequest, sceVisionManagerFinalize, sceVisionManagerGenerateRegisterUserData, sceVisionManagerGenerateRegisterUserDataFinalize, sceVisionManagerGenerateRegisterUserDataInitialize, sceVisionManagerGetCalibrateTrackingLedInfo, sceVisionManagerGetCalibrateTrackingLedNecessity, sceVisionManagerGetHitCountOfRegisterUserData, sceVisionManagerGetLibraryVersion, sceVisionManagerGetNumOfRegisterUserDataSlot, sceVisionManagerGetRegisterUserDataSize, sceVisionManagerGetRegisterUserDataVersion, sceVisionManagerGetResultOfCalibrateHmd, sceVisionManagerGetResultOfCalibrateStereoCamera, sceVisionManagerGetResultOfCalibrateTrackingLed, sceVisionManagerGetResultOfFindFace, sceVisionManagerGetResultOfFindPad, sceVisionManagerGetResultOfFindPadCorrespondRegion, sceVisionManagerGetStoreCameraImgYPtr, sceVisionManagerGetWorkingMemorySize, sceVisionManagerInitialize, sceVisionManagerRegistryClear, sceVisionManagerRequestCalibrateHmd, sceVisionManagerRequestCalibrateStereoCamera, sceVisionManagerRequestCalibrateTrackingLed, sceVisionManagerRequestClose, sceVisionManagerRequestFindFace, sceVisionManagerRequestFindPad, sceVisionManagerRequestFindPadCorrespondRegion, sceVisionManagerRequestHeadDetection, sceVisionManagerRequestInitializeRegisterUserData, sceVisionManagerRequestOpen, sceVisionManagerRequestPadTracking, sceVisionManagerRequestRecogUtility, sceVisionManagerRequestStoreCameraImgY, sceVisionManagerSetCallbackForUpdateCalibrateHmdResult, sceVisionManagerSetCallbackForUpdateCalibrateStereoCameraResult, sceVisionManagerSetCallbackForUpdateCalibrateTrackingLedResult, sceVisionManagerSetCallbackForUpdateCameraFrame, sceVisionManagerSetCallbackForUpdateCtrlCore, sceVisionManagerSetCallbackForUpdateRecog, sceVisionManagerSetCallbackForUpdateRunningStatus, sceVisionManagerSetCallbackForUpdateStoreFrame and sceVisionManagerWaitResponse](../../../core/libs/prx/libSceVisionManager/Export.cpp) (libSceVisionManager) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVisionManager.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libmdbg_syscore.md b/docs/dev/debt/libmdbg_syscore.md index cb843c4f05..0201062426 100644 --- a/docs/dev/debt/libmdbg_syscore.md +++ b/docs/dev/debt/libmdbg_syscore.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceDebugAcquireAndUpdateDebugRegister, sceDebugAttachProcess, sceDebugCancelCoredump, sceDebugClearStepThread, sceDebugCreatePerfScratchDataArea, sceDebugCreatePerfScratchExecutableArea, sceDebugCreateScratchDataArea, sceDebugCreateScratchDataAreaForPrx, sceDebugCreateScratchExecutableArea, sceDebugCreateScratchExecutableAreaForPrx, sceDebugDestroyPerfScratchDataArea, sceDebugDestroyPerfScratchExecutableArea, sceDebugDestroyScratchDataArea, sceDebugDestroyScratchExecutableArea, sceDebugDetachProcess, sceDebugGetApplicationIdByTitleId, sceDebugGetApplicationInfo, sceDebugGetApplicationList, sceDebugGetCrashInfoDetailForCoredump, sceDebugGetCrashInfoForCoredump, sceDebugGetDLLoadFlag, sceDebugGetDataListOfUltQueue, sceDebugGetDebugRegisterStatusMap, sceDebugGetEventList, sceDebugGetEventListForEQueueFd, sceDebugGetEventSubscriptionList, sceDebugGetEventSubscriptionListForEQueueFd, sceDebugGetFiberInfo, sceDebugGetFileInfo, sceDebugGetFileInfoForCoredump, sceDebugGetFileList, sceDebugGetFileListForCoredump, sceDebugGetGpuInfoArea, sceDebugGetJobManagerInfo, sceDebugGetJobManagerSequenceInfo, sceDebugGetJobManagerSequenceList, sceDebugGetModuleInfo, sceDebugGetModuleList, sceDebugGetModuleMetaData, sceDebugGetMonoVMInfo, sceDebugGetMonoVMList, sceDebugGetProcessCoredumpHandlerEventBlob, sceDebugGetProcessCoredumpHandlerEventInfo, sceDebugGetProcessCoredumpHandlerResult, sceDebugGetProcessEventCntlFlag, sceDebugGetProcessInfo, sceDebugGetProcessList, sceDebugGetProcessPropertyForCoredump, sceDebugGetProcessResourceStatCount, sceDebugGetProcessResourceStatData, sceDebugGetProcessTime, sceDebugGetProcessTimeCounter, sceDebugGetSyncExclusiveWaiterList, sceDebugGetSyncObjectData, sceDebugGetSyncObjectList, sceDebugGetSyncWaiterList, sceDebugGetSyncWaiterListForEQueueFd, sceDebugGetSystemStatusBlob, sceDebugGetSystemStatusCount, sceDebugGetThreadInfo, sceDebugGetThreadInfoByIdent, sceDebugGetThreadList, sceDebugGetThreadListAsIdent, sceDebugGetUlObjectList, sceDebugGetUltCondvarInfo, sceDebugGetUltInfo, sceDebugGetUltListOfUltRuntime, sceDebugGetUltMutexInfo, sceDebugGetUltQueueDataResourcePoolInfo, sceDebugGetUltQueueInfo, sceDebugGetUltRuntimeInfo, sceDebugGetUltRwlockInfo, sceDebugGetUltSemaphoreInfo, sceDebugGetUltWaitingQueueResourcePoolInfo, sceDebugGetVirtualMemoryDetailInfoForCoredump, sceDebugGetVirtualMemoryInfo, sceDebugGetVirtualMemoryInfoForCoredump, sceDebugGetWaitingListOfUltCondvar, sceDebugGetWaitingListOfUltMutex, sceDebugGetWaitingListOfUltQueue, sceDebugGetWaitingListOfUltRwlock, sceDebugGetWaitingListOfUltSemaphore, sceDebugGetWorkerThreadListOfUltRuntime, sceDebugInit, sceDebugInitForCoredump, sceDebugInitForTest, sceDebugIpmiGetBlockTimeInfoList, sceDebugIpmiGetBlockedIpcInfo, sceDebugIpmiGetChannelInfo, sceDebugIpmiGetChannelKidList, sceDebugIpmiGetChannelWaitingThreadList, sceDebugIpmiGetClientInfo, sceDebugIpmiGetClientKidList, sceDebugIpmiGetClientKidListByDump, sceDebugIpmiGetClientKidListByServerKid, sceDebugIpmiGetConnectRequestInfoList, sceDebugIpmiGetConnectionInfoList, sceDebugIpmiGetConnectionWaitingThreadListByClientKid, sceDebugIpmiGetConnectionWaitingThreadListBySessionKid, sceDebugIpmiGetDump, sceDebugIpmiGetEncryptedInfoAllForCoredump, sceDebugIpmiGetKidInfoListForCoredump, sceDebugIpmiGetServerDispatchInfo, sceDebugIpmiGetServerInfo, sceDebugIpmiGetServerKidList, sceDebugIpmiGetServerKidListByDump, sceDebugIpmiGetServerWaitingThreadList, sceDebugIpmiGetSessionInfo, sceDebugIpmiGetSessionKidList, sceDebugIpmiGetSessionKidListByServerKid, sceDebugIpmiGetTidListByDump, sceDebugKillApplication, sceDebugKillProcess, sceDebugNoStopChildProcesses, sceDebugNoStopOnDLLoad, sceDebugProcessSpawn, sceDebugReadEvent, sceDebugReadEventForTest, sceDebugReadProcessMemory, sceDebugReadProcessMemoryForSDBGP, sceDebugReadProcessRegister, sceDebugReadProcessRegisterForSDBGP, sceDebugReadProcessResourceStatData, sceDebugReadThreadRegister, sceDebugReadThreadRegisterForSDBGP, sceDebugReleaseDebugRegister, sceDebugResumeApplication, sceDebugResumeProcess, sceDebugResumeThread, sceDebugSetProcessEventCntlFlag, sceDebugSetStepThread, sceDebugSpawnApplication, sceDebugStopChildProcesses, sceDebugStopOnDLLoad, sceDebugSuspendApplication, sceDebugSuspendProcess, sceDebugSuspendThread, sceDebugTriggerCoredump, sceDebugTriggerCoredumpForSystem, sceDebugTriggerVrCaptureDump, sceDebugWriteProcessMemory, sceDebugWriteProcessRegister and sceDebugWriteThreadRegister](../../../core/libs/prx/libmdbg_syscore/Export.cpp) (libmdbg_syscore) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libmdbg_syscore.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceDebugAcquireAndUpdateDebugRegister, sceDebugAttachProcess, sceDebugCancelCoredump, sceDebugClearStepThread, sceDebugCreatePerfScratchDataArea, sceDebugCreatePerfScratchExecutableArea, sceDebugCreateScratchDataArea, sceDebugCreateScratchDataAreaForPrx, sceDebugCreateScratchExecutableArea, sceDebugCreateScratchExecutableAreaForPrx, sceDebugDestroyPerfScratchDataArea, sceDebugDestroyPerfScratchExecutableArea, sceDebugDestroyScratchDataArea, sceDebugDestroyScratchExecutableArea, sceDebugDetachProcess, sceDebugGetApplicationIdByTitleId, sceDebugGetApplicationInfo, sceDebugGetApplicationList, sceDebugGetCrashInfoDetailForCoredump, sceDebugGetCrashInfoForCoredump, sceDebugGetDLLoadFlag, sceDebugGetDataListOfUltQueue, sceDebugGetDebugRegisterStatusMap, sceDebugGetEventList, sceDebugGetEventListForEQueueFd, sceDebugGetEventSubscriptionList, sceDebugGetEventSubscriptionListForEQueueFd, sceDebugGetFiberInfo, sceDebugGetFileInfo, sceDebugGetFileInfoForCoredump, sceDebugGetFileList, sceDebugGetFileListForCoredump, sceDebugGetGpuInfoArea, sceDebugGetJobManagerInfo, sceDebugGetJobManagerSequenceInfo, sceDebugGetJobManagerSequenceList, sceDebugGetModuleInfo, sceDebugGetModuleList, sceDebugGetModuleMetaData, sceDebugGetMonoVMInfo, sceDebugGetMonoVMList, sceDebugGetProcessCoredumpHandlerEventBlob, sceDebugGetProcessCoredumpHandlerEventInfo, sceDebugGetProcessCoredumpHandlerResult, sceDebugGetProcessInfo, sceDebugGetProcessList, sceDebugGetProcessPropertyForCoredump, sceDebugGetProcessResourceStatCount, sceDebugGetProcessResourceStatData, sceDebugGetProcessTime, sceDebugGetProcessTimeCounter, sceDebugGetSyncExclusiveWaiterList, sceDebugGetSyncObjectData, sceDebugGetSyncObjectList, sceDebugGetSyncWaiterList, sceDebugGetSyncWaiterListForEQueueFd, sceDebugGetSystemStatusBlob, sceDebugGetSystemStatusCount, sceDebugGetThreadInfo, sceDebugGetThreadInfoByIdent, sceDebugGetThreadList, sceDebugGetThreadListAsIdent, sceDebugGetUlObjectList, sceDebugGetUltCondvarInfo, sceDebugGetUltInfo, sceDebugGetUltListOfUltRuntime, sceDebugGetUltMutexInfo, sceDebugGetUltQueueDataResourcePoolInfo, sceDebugGetUltQueueInfo, sceDebugGetUltRuntimeInfo, sceDebugGetUltRwlockInfo, sceDebugGetUltSemaphoreInfo, sceDebugGetUltWaitingQueueResourcePoolInfo, sceDebugGetVirtualMemoryDetailInfoForCoredump, sceDebugGetVirtualMemoryInfo, sceDebugGetVirtualMemoryInfoForCoredump, sceDebugGetWaitingListOfUltCondvar, sceDebugGetWaitingListOfUltMutex, sceDebugGetWaitingListOfUltQueue, sceDebugGetWaitingListOfUltRwlock, sceDebugGetWaitingListOfUltSemaphore, sceDebugGetWorkerThreadListOfUltRuntime, sceDebugInit, sceDebugInitForCoredump, sceDebugInitForTest, sceDebugIpmiGetBlockTimeInfoList, sceDebugIpmiGetBlockedIpcInfo, sceDebugIpmiGetChannelInfo, sceDebugIpmiGetChannelKidList, sceDebugIpmiGetChannelWaitingThreadList, sceDebugIpmiGetClientInfo, sceDebugIpmiGetClientKidList, sceDebugIpmiGetClientKidListByDump, sceDebugIpmiGetClientKidListByServerKid, sceDebugIpmiGetConnectRequestInfoList, sceDebugIpmiGetConnectionInfoList, sceDebugIpmiGetConnectionWaitingThreadListByClientKid, sceDebugIpmiGetConnectionWaitingThreadListBySessionKid, sceDebugIpmiGetDump, sceDebugIpmiGetEncryptedInfoAllForCoredump, sceDebugIpmiGetKidInfoListForCoredump, sceDebugIpmiGetServerDispatchInfo, sceDebugIpmiGetServerInfo, sceDebugIpmiGetServerKidList, sceDebugIpmiGetServerKidListByDump, sceDebugIpmiGetServerWaitingThreadList, sceDebugIpmiGetSessionInfo, sceDebugIpmiGetSessionKidList, sceDebugIpmiGetSessionKidListByServerKid, sceDebugIpmiGetTidListByDump, sceDebugKillApplication, sceDebugKillProcess, sceDebugNoStopChildProcesses, sceDebugNoStopOnDLLoad, sceDebugProcessSpawn, sceDebugReadEvent, sceDebugReadEventForTest, sceDebugReadProcessMemory, sceDebugReadProcessMemoryForSDBGP, sceDebugReadProcessRegister, sceDebugReadProcessRegisterForSDBGP, sceDebugReadProcessResourceStatData, sceDebugReadThreadRegister, sceDebugReadThreadRegisterForSDBGP, sceDebugReleaseDebugRegister, sceDebugResumeApplication, sceDebugResumeProcess, sceDebugResumeThread, sceDebugSetStepThread, sceDebugSpawnApplication, sceDebugStopChildProcesses, sceDebugStopOnDLLoad, sceDebugSuspendApplication, sceDebugSuspendProcess, sceDebugSuspendThread, sceDebugTriggerCoredump, sceDebugTriggerCoredumpForSystem, sceDebugTriggerVrCaptureDump, sceDebugWriteProcessMemory, sceDebugWriteProcessRegister and sceDebugWriteThreadRegister](../../../core/libs/prx/libmdbg_syscore/Export.cpp) (libmdbg_syscore) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libmdbg_syscore.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified From 262d515c8b9adb661c0b15ece2d64d36afaf35e8 Mon Sep 17 00:00:00 2001 From: hypermist Date: Thu, 22 Oct 2026 21:47:59 +1300 Subject: [PATCH 387/428] test(libs): pin the 29 firmware constant returns Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/CMakeLists.txt | 10 ++ .../tests/GuestFirmwareConstantReturns.cpp | 105 ++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 core/libs/tests/GuestFirmwareConstantReturns.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 6fdaa94178..fb0db2db9a 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -372,6 +372,16 @@ if(BUILD_TESTING) configure_windows_unwind(guest_kernel_sys_constants_tests) add_test(NAME guest_kernel_sys_constants COMMAND guest_kernel_sys_constants_tests) set_tests_properties(guest_kernel_sys_constants PROPERTIES TIMEOUT 20) + add_test_executable(guest_firmware_constant_returns_tests tests/GuestFirmwareConstantReturns.cpp) + target_include_directories(guest_firmware_constant_returns_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_firmware_constant_returns_tests PRIVATE libSceComposite libSceFsInternalForVsh libSceHttpCache libScePlayReady libSceSdma libSceSsl libSceVisionManager libmdbg_syscore libkernel libc) + configure_windows_unwind(guest_firmware_constant_returns_tests) + add_test(NAME guest_firmware_constant_returns COMMAND guest_firmware_constant_returns_tests) + set_tests_properties(guest_firmware_constant_returns PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_firmware_constant_returns PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$;PATH=path_list_prepend:$;PATH=path_list_prepend:$;PATH=path_list_prepend:$;PATH=path_list_prepend:$;PATH=path_list_prepend:$;PATH=path_list_prepend:$;PATH=path_list_prepend:$") + endif() if(Python3_Interpreter_FOUND) add_test(NAME nid_verneed COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_verneed.py $) diff --git a/core/libs/tests/GuestFirmwareConstantReturns.cpp b/core/libs/tests/GuestFirmwareConstantReturns.cpp new file mode 100644 index 0000000000..7372a257be --- /dev/null +++ b/core/libs/tests/GuestFirmwareConstantReturns.cpp @@ -0,0 +1,105 @@ +#include "SceTypes.hpp" +#include + +extern "C" { +int APS5_VABI sceCompositorCreateIndirectRenderTarget(void); +int APS5_VABI sceCompositorDeleteIndirectRenderTarget(void); +int APS5_VABI sceCompositorSetIndirectCanvasCommand(void); +int APS5_VABI sceCompositorSetIndirectCompositionCommand(void); +int APS5_VABI sceCompositorSetIndirectRenderTargetConfigCommand(void); +int APS5_VABI sceFsErriCtl(void); +int APS5_VABI sceFsErriGetState(void); +int APS5_VABI sceHttpCacheDebugSetUrl(void); +int APS5_VABI scePlayReadyMemGetStats(void); +int APS5_VABI sceSdmaInitialize(void); +int APS5_VABI sceSdmaFinalize(void); +int APS5_VABI sceSdmaQueryWorkingBufferSize(void); +int APS5_VABI sceSdmaFlushAllWithEvent(void); +int APS5_VABI sceSdmaWaitForEvent(void); +int APS5_VABI CRYPTO_uninitAsymmetricKey(void); +int APS5_VABI VLONG_freeVlongQueue(void); +int APS5_VABI sceVisionManagerOpen(void); +int APS5_VABI sceVisionManagerClose(void); +int APS5_VABI sceVisionManagerRequestFindUser(void); +int APS5_VABI sceVisionManagerRequestFindUserInRegion(void); +int APS5_VABI sceVisionManagerRequestFindUserWithoutIdentify(void); +int APS5_VABI sceVisionManagerRequestRegisterUserInRegion(void); +int APS5_VABI sceVisionManagerGetResultOfFindUser(void); +int APS5_VABI sceVisionManagerGetResultOfFindUserInRegion(void); +int APS5_VABI sceVisionManagerGetResultOfFindUserWithoutIdentify(void); +int APS5_VABI sceVisionManagerGetResultOfRegisterUserInRegion(void); +int APS5_VABI sceVisionManagerLoadSettings(void); +int APS5_VABI sceDebugGetProcessEventCntlFlag(void); +int APS5_VABI sceDebugSetProcessEventCntlFlag(void); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +void CheckComposite() { + constexpr int Unsupported = static_cast(0x80d40006); + Require(sceCompositorCreateIndirectRenderTarget() == Unsupported); + Require(sceCompositorDeleteIndirectRenderTarget() == Unsupported); + Require(sceCompositorSetIndirectCanvasCommand() == 0); + Require(sceCompositorSetIndirectCompositionCommand() == 0); + Require(sceCompositorSetIndirectRenderTargetConfigCommand() == 0); +} + +void CheckFsInternal() { + constexpr int KernelNotSupported = static_cast(0x8002002d); + Require(sceFsErriCtl() == KernelNotSupported); + Require(sceFsErriGetState() == KernelNotSupported); +} + +void CheckHttpCacheAndPlayReady() { + Require(sceHttpCacheDebugSetUrl() == 0); + Require(scePlayReadyMemGetStats() == static_cast(0x80004001)); +} + +void CheckSdma() { + constexpr int SdmaError = static_cast(0x809900c9); + Require(sceSdmaInitialize() == 0); + Require(sceSdmaFinalize() == 0); + Require(sceSdmaQueryWorkingBufferSize() == 0x8000); + Require(sceSdmaFlushAllWithEvent() == SdmaError); + Require(sceSdmaWaitForEvent() == SdmaError); +} + +void CheckSsl() { + Require(CRYPTO_uninitAsymmetricKey() == 0); + Require(VLONG_freeVlongQueue() == 0); +} + +void CheckVisionManager() { + constexpr int VisionError = static_cast(0x80e70004); + Require(sceVisionManagerOpen() == VisionError); + Require(sceVisionManagerClose() == VisionError); + Require(sceVisionManagerRequestFindUser() == VisionError); + Require(sceVisionManagerRequestFindUserInRegion() == VisionError); + Require(sceVisionManagerRequestFindUserWithoutIdentify() == VisionError); + Require(sceVisionManagerRequestRegisterUserInRegion() == VisionError); + Require(sceVisionManagerGetResultOfFindUser() == VisionError); + Require(sceVisionManagerGetResultOfFindUserInRegion() == VisionError); + Require(sceVisionManagerGetResultOfFindUserWithoutIdentify() == VisionError); + Require(sceVisionManagerGetResultOfRegisterUserInRegion() == VisionError); + Require(sceVisionManagerLoadSettings() == 0); +} + +void CheckDebugSyscore() { + Require(sceDebugGetProcessEventCntlFlag() == 1); + Require(sceDebugSetProcessEventCntlFlag() == 1); +} + +} + +int main() { + CheckComposite(); + CheckFsInternal(); + CheckHttpCacheAndPlayReady(); + CheckSdma(); + CheckSsl(); + CheckVisionManager(); + CheckDebugSyscore(); + return 0; +} From 1a0356dc104a6b1862d5db0201db67560a012817 Mon Sep 17 00:00:00 2001 From: hypermist Date: Sun, 25 Oct 2026 13:06:21 +1300 Subject: [PATCH 388/428] refactor(libs): include the library headers in the firmware constants test Co-Authored-By: Claude Opus 5.5 --- .../tests/GuestFirmwareConstantReturns.cpp | 40 ++++--------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/core/libs/tests/GuestFirmwareConstantReturns.cpp b/core/libs/tests/GuestFirmwareConstantReturns.cpp index 7372a257be..f88525c988 100644 --- a/core/libs/tests/GuestFirmwareConstantReturns.cpp +++ b/core/libs/tests/GuestFirmwareConstantReturns.cpp @@ -1,38 +1,14 @@ #include "SceTypes.hpp" +#include "prx/libSceComposite/libSceComposite.hpp" +#include "prx/libSceFsInternalForVsh/libSceFsInternalForVsh.hpp" +#include "prx/libSceHttpCache/libSceHttpCache.hpp" +#include "prx/libScePlayReady/libScePlayReady.hpp" +#include "prx/libSceSdma/libSceSdma.hpp" +#include "prx/libSceSsl/libSceSsl.hpp" +#include "prx/libSceVisionManager/libSceVisionManager.hpp" +#include "prx/libmdbg_syscore/libmdbg_syscore.hpp" #include -extern "C" { -int APS5_VABI sceCompositorCreateIndirectRenderTarget(void); -int APS5_VABI sceCompositorDeleteIndirectRenderTarget(void); -int APS5_VABI sceCompositorSetIndirectCanvasCommand(void); -int APS5_VABI sceCompositorSetIndirectCompositionCommand(void); -int APS5_VABI sceCompositorSetIndirectRenderTargetConfigCommand(void); -int APS5_VABI sceFsErriCtl(void); -int APS5_VABI sceFsErriGetState(void); -int APS5_VABI sceHttpCacheDebugSetUrl(void); -int APS5_VABI scePlayReadyMemGetStats(void); -int APS5_VABI sceSdmaInitialize(void); -int APS5_VABI sceSdmaFinalize(void); -int APS5_VABI sceSdmaQueryWorkingBufferSize(void); -int APS5_VABI sceSdmaFlushAllWithEvent(void); -int APS5_VABI sceSdmaWaitForEvent(void); -int APS5_VABI CRYPTO_uninitAsymmetricKey(void); -int APS5_VABI VLONG_freeVlongQueue(void); -int APS5_VABI sceVisionManagerOpen(void); -int APS5_VABI sceVisionManagerClose(void); -int APS5_VABI sceVisionManagerRequestFindUser(void); -int APS5_VABI sceVisionManagerRequestFindUserInRegion(void); -int APS5_VABI sceVisionManagerRequestFindUserWithoutIdentify(void); -int APS5_VABI sceVisionManagerRequestRegisterUserInRegion(void); -int APS5_VABI sceVisionManagerGetResultOfFindUser(void); -int APS5_VABI sceVisionManagerGetResultOfFindUserInRegion(void); -int APS5_VABI sceVisionManagerGetResultOfFindUserWithoutIdentify(void); -int APS5_VABI sceVisionManagerGetResultOfRegisterUserInRegion(void); -int APS5_VABI sceVisionManagerLoadSettings(void); -int APS5_VABI sceDebugGetProcessEventCntlFlag(void); -int APS5_VABI sceDebugSetProcessEventCntlFlag(void); -} - namespace { void Require(bool value) { if (!value) std::abort(); } From f2f526709cb3f9c1b88c74ffe631122707faa75c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 25 Oct 2026 11:23:05 +0100 Subject: [PATCH 389/428] docs(libs): update the libSceSdma pending export count after 5 constant returns Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceSdma.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceSdma.md b/docs/dev/debt/libSceSdma.md index 78b6855431..bef40fd068 100644 --- a/docs/dev/debt/libSceSdma.md +++ b/docs/dev/debt/libSceSdma.md @@ -2,4 +2,4 @@ ### Unknown function info -- [libSceSdma pending exports](../../../core/libs/prx/libSceSdma/Export.cpp) - 26 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSdma.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceSdma pending exports](../../../core/libs/prx/libSceSdma/Export.cpp) - 21 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSdma.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. From e6b07323de0eceb54b874a2a1e5a7e6f0658767e Mon Sep 17 00:00:00 2001 From: Gruzzberry Date: Thu, 22 Oct 2026 08:58:42 +0100 Subject: [PATCH 390/428] feat(libkernel_sys): implement settimeofday FreeBSD 11.4's kern_settimeofday rejects a timeval with a microsecond field outside 0 to 999999 or a negative second field with EINVAL before changing anything, sets the clock from a valid timeval, and then stores the timezone, which gettimeofday reports. settimeofday now checks the timeval the same way and throws for a valid one, as clock_settime does, because the host clock is not changed. A timezone given without a timeval is stored and gettimeofday reports it instead of 0 and 0. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Time/Time.cpp | 24 +++++++++++++++-- .../prx/libkernel_sys/Export.Discovery.cpp | 5 ---- core/libs/prx/libkernel_sys/Export.cpp | 6 +++++ core/libs/prx/libkernel_sys/include/Api.hpp | 2 +- core/libs/tests/GuestKernelSys.cpp | 27 +++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 6 files changed, 57 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 4e3d97ebca..63be3871dd 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -501,6 +501,9 @@ int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp) { return 0; } +static std::mutex guestTimezoneMutex; +static KernelTimezone guestTimezone{0, 0}; + int APS5_VABI gettimeofday_nid_postfix(KernelTimeval* tv, KernelTimezone* tz) { KernelTimeval ignoredTime{}; if (tv == nullptr) tv = &ignoredTime; @@ -520,8 +523,8 @@ int APS5_VABI gettimeofday_nid_postfix(KernelTimeval* tv, KernelTimezone* tz) { tv->tv_usec = static_cast(native.tv_usec); #endif if (tz != nullptr) { - tz->tz_minuteswest = 0; - tz->tz_dsttime = 0; + std::lock_guard lock(guestTimezoneMutex); + *tz = guestTimezone; } return 0; } @@ -624,6 +627,23 @@ int APS5_VABI clock_settime_nid_postfix(int clockId, const KernelTimespec* tp) { throw std::runtime_error(std::string("clock_settime: setting CLOCK_REALTIME to ") + requested + " is not supported, the host clock is not changed"); } +int SetTimeOfDay_nid_no_patch(const KernelTimeval* tv, const KernelTimezone* tz) { + if (tv != nullptr && (tv->tv_usec < 0 || tv->tv_usec >= 1000000 || tv->tv_sec < 0)) { + *__error_nid_postfix() = GUEST_EINVAL; + return -1; + } + if (tv != nullptr) { + char requested[48]; + std::snprintf(requested, sizeof(requested), "%lld.%06lld", static_cast(tv->tv_sec), static_cast(tv->tv_usec)); + throw std::runtime_error(std::string("settimeofday: setting the time to ") + requested + " is not supported, the host clock is not changed"); + } + if (tz != nullptr) { + std::lock_guard lock(guestTimezoneMutex); + guestTimezone = *tz; + } + return 0; +} + // --------------------------------------------------------------------------- // Moved as-is (not yet implemented) from the monolithic libkernel/Export.cpp. // --------------------------------------------------------------------------- diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 085e5961f0..b32a0a4639 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -2300,11 +2300,6 @@ int APS5_VABI setlogin_nid_postfix(void) { return 0; } -int APS5_VABI settimeofday_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sigqueue_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 4125e11eea..4aa5f49a80 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -515,6 +515,12 @@ int APS5_VABI fchdir_nid_postfix(int fd) { return 0; } +int SetTimeOfDay_nid_no_patch(const KernelTimeval* tv, const KernelTimezone* tz); + +int APS5_VABI settimeofday_nid_postfix(const KernelTimeval* tv, const KernelTimezone* tz) { + return SetTimeOfDay_nid_no_patch(tv, tz); +} + int APS5_VABI chroot_nid_postfix(const char* path) { std::string copy; if (const int error = CopyPath(path, copy)) return static_cast(Fail(error)); diff --git a/core/libs/prx/libkernel_sys/include/Api.hpp b/core/libs/prx/libkernel_sys/include/Api.hpp index 938bc8178b..60bea387ec 100644 --- a/core/libs/prx/libkernel_sys/include/Api.hpp +++ b/core/libs/prx/libkernel_sys/include/Api.hpp @@ -62,6 +62,7 @@ int APS5_VABI chown_nid_postfix(const char* path, std::uint32_t uid, std::uint32 int APS5_VABI lchown_nid_postfix(const char* path, std::uint32_t uid, std::uint32_t gid); int APS5_VABI fchownat_nid_postfix(int fd, const char* path, std::uint32_t uid, std::uint32_t gid, int flag); int APS5_VABI fstatat_nid_postfix(int fd, const char* path, FileStat* sb, int flag); +int APS5_VABI settimeofday_nid_postfix(const KernelTimeval* tv, const KernelTimezone* tz); int APS5_VABI unlinkat_nid_postfix(int fd, const char* path, int flag); int APS5_VABI scePthreadCondattrGetclock(const PthreadCondattr* attr, KernelClockid* clockId); int APS5_VABI renameat_nid_postfix(int oldfd, const char* from, int newfd, const char* to); @@ -534,7 +535,6 @@ int APS5_VABI sched_setscheduler_nid_postfix(void); int APS5_VABI sem_setname_nid_postfix(void); int APS5_VABI set_phys_fmem_limit_nid_postfix(void); int APS5_VABI setlogin_nid_postfix(void); -int APS5_VABI settimeofday_nid_postfix(void); int APS5_VABI sigqueue_nid_postfix(void); int APS5_VABI sigreturn_nid_postfix(void); int APS5_VABI statfs_nid_postfix(void); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 81997c91e1..ffe0e31409 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -1,6 +1,7 @@ #include "prx/libkernel_sys/include/Api.hpp" #include "prx/libkernel/include/Api.hpp" #include "prx/libkernel/Pthread/include/Cond.hpp" +#include "prx/libkernel/Time/include/Time.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libc/include/Api.hpp" #include "SceTypes.hpp" @@ -378,6 +379,31 @@ void CheckPathconf(const std::filesystem::path& root, const std::string& file) { Require(pathconf_nid_postfix(longPath.c_str(), 4) == -1 && Error() == 63); } +void CheckSettimeofday() { + KernelTimezone zone{-1, -1}; + Require(settimeofday_nid_postfix(nullptr, nullptr) == 0); + Require(gettimeofday_nid_postfix(nullptr, &zone) == 0 && zone.tz_minuteswest == 0 && zone.tz_dsttime == 0); + const KernelTimezone west{60, 1}; + for (const KernelTimeval invalid : {KernelTimeval{0, 1000000}, KernelTimeval{0, -1}, KernelTimeval{-1, 0}}) + Require(settimeofday_nid_postfix(&invalid, &west) == -1 && Error() == 22); + Require(gettimeofday_nid_postfix(nullptr, &zone) == 0 && zone.tz_minuteswest == 0 && zone.tz_dsttime == 0); + const KernelTimeval valid{1000000000, 0}; + bool rejected = false; + try { + settimeofday_nid_postfix(&valid, &west); + } catch (const std::runtime_error& error) { + rejected = std::strstr(error.what(), "settimeofday: setting the time to 1000000000.000000") != nullptr; + } + Require(rejected); + Require(gettimeofday_nid_postfix(nullptr, &zone) == 0 && zone.tz_minuteswest == 0 && zone.tz_dsttime == 0); + Require(settimeofday_nid_postfix(nullptr, &west) == 0); + KernelTimeval now{}; + Require(gettimeofday_nid_postfix(&now, &zone) == 0 && zone.tz_minuteswest == 60 && zone.tz_dsttime == 1 && now.tv_sec > 0); + const KernelTimezone zero{0, 0}; + Require(settimeofday_nid_postfix(nullptr, &zero) == 0); + Require(gettimeofday_nid_postfix(nullptr, &zone) == 0 && zone.tz_minuteswest == 0 && zone.tz_dsttime == 0); +} + void DirectoryRenameTests(const std::filesystem::path& parent) { const auto fixture = parent / "directory-identity"; const auto original = fixture / "original"; @@ -711,6 +737,7 @@ int main() { Require(link_nid_postfix(nullptr, linked.c_str()) == -1 && Error() == 14); Require(link_nid_postfix(original.c_str(), "") == -1 && Error() == 2); Require(std::filesystem::remove(linked) && std::filesystem::hard_link_count(original) == 1); + CheckSettimeofday(); const int directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); Require(directory >= 0); const int fileDescriptor = sceKernelOpen(file.c_str(), SCE_KERNEL_O_RDONLY, 0); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 4e1851fb4e..2e5af27a08 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -280,6 +280,7 @@ - [sysctl kern.arandom](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) serves FreeBSD 11's MIB {1, 37} (CTL_KERN, KERN_ARANDOM) and the "kern.arandom" sysctlbyname lookup through std::random_device, returning min(*oldLength, 256) random bytes. Following FreeBSD's sysctl_kern_arnd, a null buffer reports min(*oldLength, 256) without writing, a null length returns 0 without writing, and requests with a new value return EPERM (1) as the node is read-only. Cryptographic quality depends on the host's random device implementation. - [pthread_atfork](../../../core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp) (libkernel) appends the three handlers to a process-wide list and returns 0, or `ENOMEM` when the entry cannot be allocated, as FreeBSD 11.4's [`_pthread_atfork`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_fork.c#L78-L100) does; null handlers are accepted. The handlers never run: `fork` throws and `vfork` is absent, so no child process is created. Handlers of a module that is unloaded stay in the list, where FreeBSD's `__pthread_cxa_finalize` removes them. PPSA97358 has a source-level call from the constructor `rcu_init` ([rcu.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/rcu.c#L488-L495)), but current-main runtime reachability is unverified - [sceKernelGetPageTableStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) reports 8192 entries on each side and counts them as a PS5 (firmware 13.60) counted them for a payload process: one cpu entry for each 2 MiB granule that holds a committed mapping, whatever its protection (none included), and one gpu entry for each 1 GiB region that holds a GPU-accessible mapping, plus one for each 2 MiB granule of such a mapping that is not a whole granule of direct memory at a 2 MiB-aligned physical address. A granule or region shared by several mappings counts once, and reservations do not count. On the console the first GPU mapping of a process also takes three gpu entries and one cpu entry that stay after it is unmapped, which is not modelled, and only the ranges whose protection is recorded here are counted. A null pointer returns `SCE_KERNEL_ERROR_EFAULT`, where the console faults. Resonance: A Plague Tale Legacy (PPSA30601) calls it at start-up (#4067) +- [settimeofday](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel_sys) checks like FreeBSD 11.4's [`kern_settimeofday`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_time.c#L688-L707) for a root caller: `EINVAL` for a negative second count or a microsecond count outside 0 to 999999, before anything is stored. A valid time throws, naming it, since the host clock is never set, and its time zone is not stored, as FreeBSD stores the zone only after setting the clock. A time zone without a time is kept for the process and reported by `gettimeofday`; `sceKernelGettimezone` still reports 0 and 0, since whether the console reads the same value is not known. FreeBSD checks `PRIV_SETTIMEOFDAY` first, which a jailed process fails with `EPERM`; whether a title holds it on the console is not known. - [clock_settime](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) validates like FreeBSD 11.4's [`kern_clock_settime`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_time.c#L396-L412) for a root caller: `EFAULT` for a null time, `EINVAL` for any clock but `CLOCK_REALTIME`, a negative second count or a nanosecond count outside 0 to 999999999. A valid `CLOCK_REALTIME` request throws, naming the requested time, since the host clock is never set and no guest clock offset is kept. FreeBSD checks `PRIV_CLOCK_SETTIME` first, which a jailed process fails with `EPERM`; whether a title holds it on the console is unverified. PS5MC (PPSA99420) imports it; no call site is known - [Short sleeps ended by GPU and flip completions](../../../core/libs/prx/libkernel/Time/SleepWake.cpp) (libkernel) - a `sceKernelUsleep`, `usleep`, `nanosleep` or `sceKernelNanosleep` of 1 ms or less returns before the requested time, which FreeBSD and the console do not do: it ends once the AGC driver reports a completed GPU batch or batch completion action, or VideoOut completes a flip, so a title that polls a GPU label or the flip status between sleeps does not sleep the rest. A title that paces frames with such a sleep can wake up to 1 ms early. A thread returns early at most 8 times in a run of sleeps; the run restarts when the thread submits AGC work or did not sleep the requested time since its last sleep, so a loop that counts sleeps to time out still sleeps all but 8 in full. `APS5_NO_SLEEP_WAKE=1` keeps every sleep in full - [chflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) follows FreeBSD 11.4's [`kern_chflagsat`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2366-L2383) and UFS's [`ufs_setattr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/ufs/ufs/ufs_vnops.c#L509-L556) for a root caller: the path lookup reports `EFAULT`, `ENOENT` and `ENOTDIR`, a flag outside the 17 UFS stores (`VNOVAL` included) `EOPNOTSUPP`, and `SF_SNAPSHOT` `EPERM`. Neither host keeps BSD file flags, so every file is taken to have none (`stat` reports `st_flags` 0): flags 0 succeed without touching the file, and any other flag throws, naming the flags. `UF_READONLY`, `UF_HIDDEN`, `UF_SYSTEM` and `UF_ARCHIVE` are FreeBSD's names for the Windows attributes but are not mapped to them: UFS stores them without enforcing them, `FILE_ATTRIBUTE_READONLY` already carries the owner write bit of `chmod` on Windows, and Linux has no equivalent a process can set without `CAP_LINUX_IMMUTABLE`. The console's filesystem types, `EROFS` on read-only mounts and the change-time update are not modelled From 35ca4f26f07b7ea54d013ebd92fa83ab3b902152 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sun, 25 Oct 2026 16:27:00 +0300 Subject: [PATCH 391/428] fix(nid): reject sectionless dynamic executables --- core/libs/nid/src/ElfNidPatcher.cpp | 14 ++++++++++++- core/libs/nid/tests/test_pe_references.py | 25 +++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/core/libs/nid/src/ElfNidPatcher.cpp b/core/libs/nid/src/ElfNidPatcher.cpp index a8e3f24f9a..2e06bc5431 100644 --- a/core/libs/nid/src/ElfNidPatcher.cpp +++ b/core/libs/nid/src/ElfNidPatcher.cpp @@ -400,7 +400,19 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& throw std::runtime_error("unsupported .gnu.version_d section: symbol version definitions cannot be preserved"); } - if (options.executable && dynSymOffset == 0u && !hasDynamic) return; + if (options.executable && dynSymOffset == 0u && !hasDynamic) { + if (ehdr.e_phnum) { + if (!ehdr.e_phoff || ehdr.e_phentsize != sizeof(Elf64_Phdr) || ehdr.e_phnum == 0xffffu) + throw std::runtime_error("unsupported ELF program header layout"); + RequireElfRange(elf, ehdr.e_phoff, std::uint64_t{ehdr.e_phnum} * sizeof(Elf64_Phdr), "program headers"); + for (std::uint16_t index = 0u; index < ehdr.e_phnum; ++index) { + const auto segment = Read(elf, ehdr.e_phoff + index * sizeof(Elf64_Phdr)); + if (segment.p_type == 2u) + throw std::runtime_error("unsupported ELF PT_DYNAMIC without a .dynsym section"); + } + } + return; + } if (dynSymOffset == 0u) throw std::runtime_error("no .dynsym section"); const std::size_t symCount = dynSymSize / sizeof(Elf64_Sym); diff --git a/core/libs/nid/tests/test_pe_references.py b/core/libs/nid/tests/test_pe_references.py index 0489b2b26c..2992a55093 100644 --- a/core/libs/nid/tests/test_pe_references.py +++ b/core/libs/nid/tests/test_pe_references.py @@ -351,6 +351,31 @@ class ExecutableImportTests(unittest.TestCase): result = subprocess.run([str(PATCHER), "--exec", str(path)], capture_output=True, text=True, timeout=20) self.assertEqual(result.returncode, 0, result.stderr) + def test_elf_without_sections_rejects_dynamic_segment_without_writing(self): + image = self.elf() + struct.pack_into(" Date: Sat, 24 Oct 2026 11:51:50 +0200 Subject: [PATCH 392/428] feat(libkernel): implement getlogin Co-Authored-By: Claude Sonnet 5.5 --- core/libs/CMakeLists.txt | 9 +++ core/libs/prx/libkernel/CMakeLists.txt | 1 + .../prx/libkernel/System/src/Getlogin.cpp | 12 +++ core/libs/prx/libkernel/System/src/Login.cpp | 17 +++++ core/libs/prx/libkernel/include/Api.hpp | 1 + .../prx/libkernel_sys/Export.Discovery.cpp | 5 -- core/libs/prx/libkernel_sys/include/Api.hpp | 1 - core/libs/tests/GuestKernelSysLogin.cpp | 76 +++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 9 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 core/libs/prx/libkernel/System/src/Getlogin.cpp create mode 100644 core/libs/tests/GuestKernelSysLogin.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 6fdaa94178..25e1ce9b59 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -436,6 +436,15 @@ if(BUILD_TESTING) add_test(NAME windows_write_watch COMMAND windows_write_watch_tests) set_tests_properties(windows_write_watch PROPERTIES TIMEOUT 20) endif() + add_test_executable(guest_kernel_sys_login_tests tests/GuestKernelSysLogin.cpp) + target_include_directories(guest_kernel_sys_login_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_login_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_login_tests) + add_test(NAME guest_kernel_sys_login COMMAND guest_kernel_sys_login_tests) + if(WIN32) + set_tests_properties(guest_kernel_sys_login PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() if(WIN32) add_test_executable(undeliverable_host_exception_tests tests/UndeliverableHostException.cpp) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 9da2728d50..5ffb8d4625 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -69,6 +69,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/System/src/HostCpu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SystemConfiguration.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Login.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Getlogin.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SoftwareVersion.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Error.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/ThermalSensors.cpp diff --git a/core/libs/prx/libkernel/System/src/Getlogin.cpp b/core/libs/prx/libkernel/System/src/Getlogin.cpp new file mode 100644 index 0000000000..c795555bcb --- /dev/null +++ b/core/libs/prx/libkernel/System/src/Getlogin.cpp @@ -0,0 +1,12 @@ +#include "prx/libc/include/Api.hpp" +#include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +extern "C" char* APS5_VABI getlogin_nid_postfix(void) { + static char logname[33]; + if (const int error = getlogin_r_nid_postfix(logname, static_cast(sizeof(logname))); error != 0) { + *__error_nid_postfix() = error; + return nullptr; + } + return logname[0] != '\0' ? logname : nullptr; +} diff --git a/core/libs/prx/libkernel/System/src/Login.cpp b/core/libs/prx/libkernel/System/src/Login.cpp index e188559977..82f71f218b 100644 --- a/core/libs/prx/libkernel/System/src/Login.cpp +++ b/core/libs/prx/libkernel/System/src/Login.cpp @@ -3,6 +3,9 @@ #include #include #include +#include +#include +#include #include #ifdef _WIN32 #include @@ -13,7 +16,16 @@ #endif namespace { +std::optional g_loginForTest; + int ReadLogin(char* output, int size) { + if (g_loginForTest) { + if (g_loginForTest->empty()) throw std::runtime_error("getlogin_r: host effective user has no account name"); + const std::size_t length = g_loginForTest->size() + 1; + if (length > static_cast(size)) return 34; + std::memcpy(output, g_loginForTest->c_str(), length); + return 0; + } #ifdef _WIN32 std::array name{}; DWORD count = static_cast(name.size()); @@ -44,6 +56,11 @@ int ReadLogin(char* output, int size) { } } +extern "C" void LoginNameForTest_nid_no_patch(const char* name) { + if (name == nullptr) g_loginForTest.reset(); + else g_loginForTest = name; +} + extern "C" int APS5_VABI getlogin_r_nid_postfix(char* output, int size) { if (size <= 0) return 34; if (output == nullptr) return 14; diff --git a/core/libs/prx/libkernel/include/Api.hpp b/core/libs/prx/libkernel/include/Api.hpp index 35b536672a..199d218706 100644 --- a/core/libs/prx/libkernel/include/Api.hpp +++ b/core/libs/prx/libkernel/include/Api.hpp @@ -646,6 +646,7 @@ int APS5_VABI sceKernelRaiseException(Pthread thread, int signum); void APS5_VABI sceKernelDebugRaiseException(int c1, int c2); void APS5_VABI sceKernelDebugRaiseExceptionOnReleaseMode(int c1, int c2); int APS5_VABI getlogin_r_nid_postfix(char* output, int size); +char* APS5_VABI getlogin_nid_postfix(void); int APS5_VABI sceKernelDebugOutText(int channel, const char* text); int APS5_VABI sceKernelSendNotificationRequest(int device, const void* request, std::size_t size, int flags); int APS5_VABI getargc_nid_postfix(void); diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 085e5961f0..88c569bccf 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -102,11 +102,6 @@ int APS5_VABI getfsstat_nid_postfix(void) { return 0; } -int APS5_VABI getlogin_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI ipmimgr_call_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel_sys/include/Api.hpp b/core/libs/prx/libkernel_sys/include/Api.hpp index 938bc8178b..3a544c07b0 100644 --- a/core/libs/prx/libkernel_sys/include/Api.hpp +++ b/core/libs/prx/libkernel_sys/include/Api.hpp @@ -87,7 +87,6 @@ int APS5_VABI get_sdk_compiled_version_nid_postfix(void); int APS5_VABI get_self_auth_info_nid_postfix(void); int APS5_VABI get_vm_map_timestamp_nid_postfix(void); int APS5_VABI getfsstat_nid_postfix(void); -int APS5_VABI getlogin_nid_postfix(void); int APS5_VABI ipmimgr_call_nid_postfix(void); int APS5_VABI is_in_sandbox_nid_postfix(void); int APS5_VABI kenv_nid_postfix(void); diff --git a/core/libs/tests/GuestKernelSysLogin.cpp b/core/libs/tests/GuestKernelSysLogin.cpp new file mode 100644 index 0000000000..44c5f217ed --- /dev/null +++ b/core/libs/tests/GuestKernelSysLogin.cpp @@ -0,0 +1,76 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +void LoginNameForTest_nid_no_patch(const char* name); +} + +namespace { + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "check failed at line %u\n", static_cast(location.line())); + std::exit(1); +} + +int Error() { + return *__error_nid_postfix(); +} + +void Getlogin() { + char expected[33]{}; + const int error = getlogin_r_nid_postfix(expected, static_cast(sizeof(expected))); + char* name = getlogin_nid_postfix(); + if (error != 0) { + Require(name == nullptr && Error() == error); + return; + } + if (expected[0] == '\0') { + Require(name == nullptr); + return; + } + Require(name != nullptr && std::strcmp(name, expected) == 0); + Require(getlogin_nid_postfix() == name); +} + +void GetloginPaths() { + LoginNameForTest_nid_no_patch("player"); + char* name = getlogin_nid_postfix(); + Require(name != nullptr && std::strcmp(name, "player") == 0); + char buffer[7]; + std::memset(buffer, 'x', sizeof(buffer)); + Require(getlogin_r_nid_postfix(buffer, 6) == 34 && buffer[0] == 'x'); + Require(getlogin_r_nid_postfix(buffer, 7) == 0 && std::strcmp(buffer, "player") == 0); + + const std::string longName(40, 'n'); + LoginNameForTest_nid_no_patch(longName.c_str()); + *__error_nid_postfix() = 0; + Require(getlogin_nid_postfix() == nullptr && Error() == 34); + + LoginNameForTest_nid_no_patch(""); + bool threw = false; + try { + getlogin_nid_postfix(); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + LoginNameForTest_nid_no_patch(nullptr); +} + +} + +int main() { + Getlogin(); + GetloginPaths(); + std::puts("getlogin passed"); + return 0; +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 4e1851fb4e..c02f2d2c00 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -19,6 +19,7 @@ - [sceKernelError](../../../core/libs/prx/libkernel/System/src/Error.cpp) follows [shadPS4's kernel implementation](https://github.com/shadps4-emu/shadPS4/blob/23c41333822fe8eca4c609b43119180af63ae4ac/src/core/libraries/kernel/kernel.cpp): zero returns zero; other guest POSIX error values receive the `0x80020000` bias without modifying errno. Arithmetic is defined modulo 32 bits to avoid signed overflow for inputs outside the errno domain. Console behavior for those inputs and PS5 equivalence have not been measured. - [physhm_open, physhm_unlink](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel, NIDs `n371J5cP+uo` and `AUqJNkobQ1c`) - declared as throwing stubs: the PS5 physical shared memory API is not documented, and [shadPS4's module generator](https://github.com/shadps4-emu/moduleGenerator/blob/main/libraries/kernel.cpp) also declares both with no arguments and returns `ORBIS_OK`, so the signature, the handle a success would return and the console's behaviour are unmeasured. The no-argument `int` signature follows shadPS4. Forever Launcher (PPSA99030) imports both and stops at load on `physhm_open`, and the relinker resolves no call site for either in that title, so no handle is fabricated and both throw - [getlogin_r](../../../core/libs/prx/libkernel/System/src/Login.cpp) (libkernel) returns the host effective account name: `GetUserNameW` as UTF-8 on Windows and `getpwuid_r(geteuid())` on POSIX. A valid account returns a name even without a login session; host `getlogin_r`, `USER` and `LOGNAME` are not consulted. The console's login name and behaviour without a session are unknown. Account lookup or encoding failures, a missing or empty account name, and POSIX account records exceeding the 16 KiB lookup buffer throw; there is no replacement name or retry. A short guest buffer returns `ERANGE` without modifying it, and a null output returns `EFAULT`. +- [getlogin](../../../core/libs/prx/libkernel/System/src/Getlogin.cpp) (libkernel) follows FreeBSD 11 `lib/libc/gen/getlogin.c` and returns a pointer to one static 33-byte buffer filled by `getlogin_r`; on an error it sets `errno` to `getlogin_r`'s code and returns NULL. These semantics are not checked against a PS5. It exports the test seam `LoginNameForTest_nid_no_patch`, which replaces the host account name for tests. - [sceKernelGetSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports baseline system software version `0x01000000` ("01.000.000") for ProsperoStore (PPSA99000); the console's firmware version is not modeled. Measured on PS5 firmware 13.42: record values 0, 24, 39, 40, 41, 48 and 64 in the leading 64-bit field are accepted and preserved, the 28-byte string at offset 8 and packed version at 0x24 are filled, and bytes from 0x28 onward remain untouched. This API reports "13.590.001" / `0x13590001` in that environment, while the Prospero variant reports "13.420.000" / `0x13420000`; their values must not be assumed identical. A null pointer still returns `SCE_KERNEL_ERROR_EFAULT`, which was not measured; other firmware versions and record values remain unverified. - [kern.rng_pseudo](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` draws a new block of 64 bytes from the host's `std::random_device` for each read (no new value) that passes a buffer and a length, and sizes its answers as a payload process on a PS5 (firmware 12.70) sees it: a size query reports 64; a buffer of 64 bytes or more gets 64 bytes and a length of 64 (tried up to 65536); a smaller buffer gets as many bytes as its length, which is left as given, and a null length gets none, both with -1 and `ENOMEM`; a null buffer with a null length returns 0. `errno` is kept when the call succeeds. A write returns -1 with `EPERM`, which was not tried on the console. The only caller seen is mbedTLS's `mbedtls_hardware_poll` in the catalog's ProsperoLight boilerplate, which asks for 64-byte blocks and fails if fewer come back. The console's generator is unknown, and a title process was not measured. On the console `sysctlnametomib` gives MIB {1, 50} for the name, and `sysctl` with it answers reads of 0, 1, 63, 64, 65 and 128 bytes, a size query, a null length and a null buffer with a null length as the name does; here `sysctlnametomib` maps the name to {1, 50} and `sysctl` {1, 50} answers as the name does; a write through the MIB was not measured and throws - [sysconf](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `_SC_CLK_TCK` returns 128, FreeBSD's `CLK_TCK` as its [`sysconf`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/sysconf.c#L100-L101) returns it ([`time.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/include/time.h#L48-L54)); the console's value is not checked, and no `times` reports ticks at that rate. CPython's `posix` module stops its import with "cannot read ticks_per_second" when the value is below 1, as the Python in PS5MC (PPSA99420) does. `_SC_ARG_MAX` returns 262144 (FreeBSD's `NCARGS`), `_SC_OPEN_MAX` returns the `RLIMIT_NOFILE` soft limit via `getrlimit` ([`sysconf.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/sysconf.c#L106-L115)), and `_SC_IOV_MAX` returns 1024 (FreeBSD's `UIO_MAXIOV`). Names other than `_SC_ARG_MAX`, `_SC_CLK_TCK`, `_SC_OPEN_MAX`, `_SC_PAGESIZE`, `_SC_IOV_MAX`, `_SC_NPROCESSORS_CONF`, `_SC_NPROCESSORS_ONLN` and `_SC_PHYS_PAGES` fail with `EINVAL` From 9514ad821540ac9497ad8080b13e3c7483229bff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 25 Oct 2026 15:13:23 +0100 Subject: [PATCH 393/428] fix(main): settle the inotify baseline in VnodeOneshotWatchStops Culprit #7049; failing run 40193493248 (linux guest_kqueue aborted after 6 s inside VnodeOneshotWatchStops). Watch threads of the previous test close their inotify fds after Stop(), so the baseline count could drop below the final one and the exact equality never held. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestKqueue.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index 300804f790..12afea42a1 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -1219,7 +1219,13 @@ static void VnodeOneshotWatchStops() { KernelEvent events[4]{}; const auto ident = static_cast(file); #ifdef __linux__ - const auto watchesBefore = InotifyWatchCount(); + auto watchesBefore = InotifyWatchCount(); + for (int attempt = 0; attempt < 100; ++attempt) { + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + const auto settled = InotifyWatchCount(); + if (settled == watchesBefore) break; + watchesBefore = settled; + } #endif Require(Apply(kq, Change(ident, EvfiltVnode, EvAdd | EvOneshot, NoteDelete | NoteWrite, 0, &marker)) == 0); Require(write_nid_postfix(file, "abc", 3) == 3); @@ -1230,8 +1236,8 @@ static void VnodeOneshotWatchStops() { Require(Poll(kq, events, 4) == 0); RequireFailure(Apply(kq, Change(ident, EvfiltVnode, EvDelete)), GuestEnoent); #ifdef __linux__ - for (int attempt = 0; attempt < 100 && InotifyWatchCount() != watchesBefore; ++attempt) std::this_thread::sleep_for(std::chrono::milliseconds(20)); - Require(InotifyWatchCount() == watchesBefore); + for (int attempt = 0; attempt < 100 && InotifyWatchCount() > watchesBefore; ++attempt) std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(InotifyWatchCount() <= watchesBefore); #endif Require(close_nid_postfix(file) == 0); Require(close_nid_postfix(kq) == 0); From 18ee716d538ca888857ca86f1e93b77daab77814 Mon Sep 17 00:00:00 2001 From: Jamie <55252868+chitters2004@users.noreply.github.com> Date: Fri, 23 Oct 2026 04:31:15 +0100 Subject: [PATCH 394/428] fix(tools): strip forwarded dependency flags from syntax checks --- CMakeLists.txt | 3 + tools/check_compile.py | 21 +++- .../tests/test_check_compile_preprocessor.py | 101 ++++++++++++++++++ 3 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 tools/tests/test_check_compile_preprocessor.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 813ad77f6e..11039135be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -277,6 +277,9 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_alignment.py) add_test(NAME conventions_pr_template COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_pr_template.py) + add_test(NAME check_compile_preprocessor COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_compile_preprocessor.py) + set_tests_properties(check_compile_preprocessor PROPERTIES TIMEOUT 20) add_test(NAME check_compile COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_compile.py) add_test(NAME performance_compare COMMAND ${Python3_EXECUTABLE} diff --git a/tools/check_compile.py b/tools/check_compile.py index 32934f8fb3..24500ab954 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -159,18 +159,30 @@ def syntax_only(entry, source): return syntax_arguments(arguments, directory, source) -def validate_preprocessor_arguments(argument): +def preprocessor_arguments(argument): forwarded = argument[4:].split(",") if any(option.startswith("@") for option in forwarded): raise ValueError(f"unsupported forwarded compiler option response file in {argument}") options = iter(forwarded) + kept = [] for option in options: + if option in ("-MD", "-MMD", "-MF", "-MT", "-MQ", "--write-dependencies", "--write-user-dependencies"): + if not next(options, None): + raise ValueError(f"missing value for {option} in {argument}") + continue + if option in ("-M", "-MM", "-MG", "-MP", "--dependencies", "--user-dependencies", + "--print-missing-file-dependencies") or option.startswith(("-MF", "-MT", "-MQ")): + continue + kept.append(option) if option in ("-D", "-U", "-I", "-F", "-include", "--include", "-imacros", "--imacros", "-isystem", "-iquote", "-idirafter", "-isysroot"): - if not next(options, None): + value = next(options, None) + if not value: raise ValueError(f"missing operand for {option} in {argument}") + kept.append(value) elif not (option.startswith(("-D", "-U", "-I", "-F")) and len(option) > 2): raise ValueError(f"unsupported forwarded compiler option {option} in {argument}") + return "-Wp," + ",".join(kept) if kept else None def syntax_arguments(arguments, directory, source): @@ -203,7 +215,10 @@ def syntax_arguments(arguments, directory, source): index += 2 continue if not positional and argument.startswith("-Wp,"): - validate_preprocessor_arguments(argument) + if forwarded := preprocessor_arguments(argument): + trimmed.append(forwarded) + index += 1 + continue if not positional and (argument in (*DROP, *PREPROCESSING) or argument.startswith(("-MT", "-MF", "-MQ", "-MJ"))): index += 1 continue diff --git a/tools/tests/test_check_compile_preprocessor.py b/tools/tests/test_check_compile_preprocessor.py new file mode 100644 index 0000000000..0778572011 --- /dev/null +++ b/tools/tests/test_check_compile_preprocessor.py @@ -0,0 +1,101 @@ +import os +import shlex +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import check_compile + + +class CheckCompilePreprocessorTests(unittest.TestCase): + def test_forwarded_dependencies_preserve_outputs_and_compiler_errors(self): + self.check_forwarded_dependencies(shutil.which("clang") or shutil.which("gcc"), ("-MD", "-MMD")) + + def test_gcc_dependency_aliases_preserve_outputs_and_compiler_errors(self): + compiler = shutil.which("gcc") + if compiler is not None: + version = subprocess.run([compiler, "--version"], capture_output=True, text=True) + if "clang" in version.stdout.lower(): + self.skipTest("GCC dependency aliases are not supported by Clang") + self.check_forwarded_dependencies(compiler, ("--write-dependencies", "--write-user-dependencies")) + + def check_forwarded_dependencies(self, compiler, options): + if compiler is None: + self.skipTest("needs a C compiler") + with tempfile.TemporaryDirectory(prefix="syntax check ") as folder: + root = Path(folder).resolve() + source = root / "unit source.c" + dependency = root / "unit deps.d" + object_file = root / "unit.o" + valid = "#ifndef REQUIRED\n#error lost_preprocessor_definition\n#endif\nint value;\n" + for option in options: + for field in ("arguments", "command"): + with self.subTest(option=option, field=field): + source.write_text(valid, encoding="utf-8") + forwarded = [f"-Wp,{option},{dependency}", "-Wp,-DREQUIRED,-MP,-MT,custom_target,-UUNUSED"] + arguments = [compiler, "-Werror", "-c", str(source), *forwarded, "-o", str(object_file)] + original = subprocess.run(arguments, cwd=root, capture_output=True, text=True) + self.assertEqual(original.returncode, 0, original.stderr) + preserved = {path: path.read_bytes() for path in (dependency, object_file)} + command = (subprocess.list2cmdline(arguments) if os.name == "nt" else + " ".join(shlex.quote(argument) for argument in arguments)) + entry = {"directory": str(root), "file": str(source), + field: arguments if field == "arguments" else command} + for text, failures in ((valid, 0), (valid + "#error syntax_check_must_fail\n", 1)): + source.write_text(text, encoding="utf-8") + checked, failed, diagnostics = check_compile.check_source( + (source, source.name, root, [entry])) + self.assertEqual((checked, failed), (1, failures), diagnostics) + if failures: + self.assertIn("syntax_check_must_fail", diagnostics) + for path, content in preserved.items(): + self.assertEqual(path.read_bytes(), content, str(path)) + self.assertEqual(set(root.iterdir()), {source, dependency, object_file}) + dependency.unlink() + object_file.unlink() + source.write_text(valid, encoding="utf-8") + self.assertEqual(check_compile.check_source((source, source.name, root, [entry]))[:2], (1, 0)) + self.assertEqual(set(root.iterdir()), {source}) + + def test_dependency_only_arguments_are_removed(self): + source = Path("unit.c").resolve() + flags = ["-Wp,-MD,unit.d", "-Wp,-MMD,unit.d,-MP", "-Wp,-MF,unit.d,-MT,target,-MQ,quoted", + "-Wp,-MFunit.d,-MTtarget,-MQquoted", "-Wp,-M,-MM,-MG,-MP", + "-Wp,--write-dependencies,unit.d,--dependencies,--user-dependencies,--print-missing-file-dependencies", + "-Wp,--write-user-dependencies,unit.d"] + for flag in flags: + with self.subTest(flag=flag): + self.assertEqual(check_compile.syntax_arguments(["cc", flag, "-c", str(source)], Path.cwd(), source), + ["cc", "-fsyntax-only", str(source)]) + + def test_other_preprocessor_arguments_are_preserved(self): + source = Path("unit.c").resolve() + for flag, expected in (("-Wp,-DNAME=value,-MD,unit.d,-I,include files", "-Wp,-DNAME=value,-I,include files"), + ("-Wp,-include,-MD,-MMD,unit.d", "-Wp,-include,-MD"), + ("-Wp,--include,-MD,--write-dependencies,unit.d", "-Wp,--include,-MD")): + with self.subTest(flag=flag): + self.assertEqual(check_compile.syntax_arguments(["cc", flag, str(source)], Path.cwd(), source), + ["cc", "-fsyntax-only", expected, str(source)]) + + def test_unsupported_options_and_response_operands_are_still_rejected(self): + source = Path("unit.c").resolve() + for flag in ("-Wp,-unknown-option", "-Wp,-MD,unit.d,-E", "-Wp,-MD,unit.d,-I,@operand.rsp", + "-Wp,-MD,@operand.rsp"): + with self.subTest(flag=flag): + with self.assertRaisesRegex(ValueError, "unsupported forwarded compiler option"): + check_compile.syntax_arguments(["cc", flag, str(source)], Path.cwd(), source) + + def test_missing_dependency_values_are_rejected(self): + source = Path("unit.c").resolve() + for option in ("-MD", "-MMD", "-MF", "-MT", "-MQ", "--write-dependencies", "--write-user-dependencies"): + with self.subTest(option=option): + with self.assertRaisesRegex(ValueError, "missing value"): + check_compile.syntax_arguments(["cc", f"-Wp,{option}", str(source)], Path.cwd(), source) + + +if __name__ == "__main__": + unittest.main() From 4e657b158e0f81aa55e27a5af980aa751ca33d97 Mon Sep 17 00:00:00 2001 From: Maz Date: Thu, 22 Oct 2026 02:32:31 +0400 Subject: [PATCH 395/428] feat(libc): export the time_get facet vtable as data Console libSceLibcInternal exports o4kt51-uO48 as a 104-byte STT_OBJECT; titles that resolve the symbol as data need a zeroed 13-slot vtable like the other facet vtables, not a missing import. --- core/libs/prx/libc/src/LocaleSupport.cpp | 1 + core/libs/tests/GuestLocale.cpp | 9 +++++++++ docs/dev/debt/libc.md | 1 + 3 files changed, 11 insertions(+) diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index 2546de6389..a838c65929 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -383,6 +383,7 @@ std::uint64_t _ZNSt7num_putIwSt19ostreambuf_iteratorIwSt11char_traitsIwEEE2idE_n std::uintptr_t _ZTVSt7num_putIwSt19ostreambuf_iteratorIwSt11char_traitsIwEEE_nid_postfix[14] {}; std::uint64_t _ZNSt8numpunctIcE2idE_nid_postfix = 0; std::uint64_t _ZNSt8time_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0; +std::uintptr_t _ZTVSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix[13] {}; std::streamoff _ZSt7_BADOFF_nid_postfix = -1; std::fpos_t _ZSt4_Fpz_nid_postfix {}; diff --git a/core/libs/tests/GuestLocale.cpp b/core/libs/tests/GuestLocale.cpp index 3baacd92f3..a96a8a7c2a 100644 --- a/core/libs/tests/GuestLocale.cpp +++ b/core/libs/tests/GuestLocale.cpp @@ -22,6 +22,7 @@ extern std::uintptr_t _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIc extern std::uintptr_t _ZTVSt7codecvtIwc9_MbstatetE_nid_postfix[13]; extern std::uintptr_t _ZTVSt7num_putIwSt19ostreambuf_iteratorIwSt11char_traitsIwEEE_nid_postfix[14]; extern std::uintptr_t _ZTVSt7collateIcE_nid_postfix[9]; +extern std::uintptr_t _ZTVSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix[13]; } @@ -332,6 +333,13 @@ static void CheckFacetGetcat() { } Require(_ZNSt7codecvtIcc9_MbstatetE7_GetcatEPPKNSt6locale5facetEPKS2__nid_postfix(nullptr, &locale) == 2); Require(_ZNSt8numpunctIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&punct, &locale) == 4); +static void CheckTimeGetVtableObject() { + for (std::uintptr_t& slot : _ZTVSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix) { + Require(slot == 0); + slot = 0x1111; + Require(slot == 0x1111); + slot = 0; + } } int main() { @@ -357,6 +365,7 @@ int main() { CheckWideNumPutVtableObject(); CheckFacetGetcat(); CheckCollateVtableObject(); + CheckTimeGetVtableObject(); _Locksyslock_nid_postfix(); _Locksyslock_nid_postfix(); _Unlocksyslock_nid_postfix(); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index d4ce4673c9..0fce1306c0 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -165,6 +165,7 @@ - [fopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - returns the errno of the failed open, as C11 K.3.5.2.1 leaves the nonzero value open and Microsoft's CRT returns errno; the console's Dinkumware `fopen_s` is not verified - [codecvt vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, NID `H-TDszhsYuY`) is thirteen zeroed `uintptr_t` slots matching `st_size` 104 on title `libc.prx` from PPSA26344 and on `libSceLibcInternal.sprx` (offset-to-top, typeinfo, two destructors, `_Incref`/`_Decref`, and seven conversion virtuals). Facet calls through the object fault until those methods are wired. - [num_put vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, NID `59oywaaZbJk`) is fourteen zeroed `uintptr_t` slots matching `st_size` 112 on the title `libc.prx` from PPSA16833 (offset-to-top, typeinfo, two destructors, `_Incref`/`_Decref`, and eight `do_put` virtuals). Facet calls through the object fault until those methods are wired. +- [time_get vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, NID `o4kt51-uO48`) is thirteen zeroed `uintptr_t` slots matching `st_size` 104 on console `libSceLibcInternal.sprx` (offset-to-top, typeinfo, two destructors, `_Incref`/`_Decref`, and seven `do_*` virtuals). Facet calls through the object fault until those methods are wired. - [ios_base standard streams](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) `_Addstd` queues the stream in the standard stream table of Dinkumware [ios.cpp](https://github.com/ganboing/crt.vc12/blob/master/src/ios.cpp) (`stdstr`, `stdopens`, `_Addstd`, `_Ios_base_dtor`): it takes the first of slots 1 to 7 that is free or already holds the stream, and slot 8 (`NSTDSTR`) when none is, so slot 8 is shared by every stream past the first seven and registration never fails. It stores the 1-based slot index in the guest `IosBase::standardStream` (Dinkumware's `_Stdstr`, where 0 means not a standard stream) and counts the stream objects registered per slot; registering the same stream again reuses its slot and raises its count. Measured on a PS5 (firmware 13.60) from a homebrew payload calling the system `libSceLibcInternal` export: in a process whose own standard streams already hold slots 1 to 7, twelve distinct streams and a repeated one all get 8, written as a 64-bit value at offset 0x8 with no other byte of the object touched; the assignment of slots 1 to 7 to fresh streams is not observable there and follows VC12. The counts are `int`, not VC12's `char`, and the registration and the destructor lock the table where VC12 locks only `_Addstd`; a slot index past the table throws in the destructor instead of indexing out of bounds. The destructor decrements its slot's count and suppresses the rest of the destruction while the count stays positive, as `_Ios_base_dtor` does, and the slots of destroyed streams keep their pointer and stay reserved for that stream, as `stdstr` does. The streams themselves are destroyed by the title's own code as it registers them through `__cxa_atexit`; when the last reference closes, the unsupported stream storage and callback cleanup is still the throw it was and the locale is released as for a non-standard stream. - [std::_Pad, _Thrd_join, _Thrd_detach, _Thrd_id](../../../core/libs/prx/libc/src/Threads.cpp) (libc) follow Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. `_Thrd_id` throws because whether the console returns `pthread_t` or a kernel thread ID and at what width is unknown; `_Launch` omits `_Cnd_do_broadcast_at_thread_exit` and calls `pthread_create` registered by libkernel with default attributes. The 32-byte pad layout (`_Go()` vtable at 0x0, `_Cnd_t` at 0x8, `_Mtx_t` at 0x10, `bool _Started` at 0x18) is assumed from VC12, unverified. Failing primitives or thread start throw the guest `std::system_error`; null arguments throw. `_Thrd_join` returns `_Thrd_error` (4) on `pthread_join` failure and truncates the exit code to `int`; `_Thrd_detach` returns `_Thrd_error` (4) on `pthread_detach` failure and throws when none is registered - [siginterrupt](../../../core/libs/prx/libc/src/Process.cpp) (libc) runs FreeBSD 11.4's `siginterrupt.c` sequence through the `sigaction` and the `_sigintr` set that libkernel registers at load: it reads the signal's disposition, sets or clears the signal's bit and `SA_RESTART`, and installs the disposition again. The set is written outside libkernel's registration lock, as FreeBSD's libc writes it without one; a call before libkernel has registered throws. From 159a6f44078e56a677b354da7e0d88688583a7f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 25 Oct 2026 15:20:11 +0100 Subject: [PATCH 396/428] test(libc): close CheckFacetGetcat before the time_get vtable check --- core/libs/tests/GuestLocale.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestLocale.cpp b/core/libs/tests/GuestLocale.cpp index a96a8a7c2a..64a41353cf 100644 --- a/core/libs/tests/GuestLocale.cpp +++ b/core/libs/tests/GuestLocale.cpp @@ -333,6 +333,8 @@ static void CheckFacetGetcat() { } Require(_ZNSt7codecvtIcc9_MbstatetE7_GetcatEPPKNSt6locale5facetEPKS2__nid_postfix(nullptr, &locale) == 2); Require(_ZNSt8numpunctIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&punct, &locale) == 4); +} + static void CheckTimeGetVtableObject() { for (std::uintptr_t& slot : _ZTVSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix) { Require(slot == 0); From f66b095adadc6b758eadb7f90c4594e8531e498d Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 26 Oct 2026 03:24:02 +1300 Subject: [PATCH 397/428] test(savedata): unwind the null-input throw on Windows guest_savedata_delete catches the runtime_error that the PRX raises for null arguments, so the target needs configure_windows_unwind like the other savedata targets that catch; without it the throw terminates the process on MinGW. --- core/libs/prx/libSceSaveData.native/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceSaveData.native/CMakeLists.txt b/core/libs/prx/libSceSaveData.native/CMakeLists.txt index 6b03a2c86c..46d6b8bba9 100644 --- a/core/libs/prx/libSceSaveData.native/CMakeLists.txt +++ b/core/libs/prx/libSceSaveData.native/CMakeLists.txt @@ -16,6 +16,7 @@ if(BUILD_TESTING) add_test_executable(guest_savedata_delete_tests ${LIBS_INCLUDE_DIR}/tests/GuestSaveDataDelete.cpp) target_include_directories(guest_savedata_delete_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_savedata_delete_tests PRIVATE libSceSaveData.native libc) + configure_windows_unwind(guest_savedata_delete_tests) add_test(NAME guest_savedata_delete COMMAND guest_savedata_delete_tests) if(WIN32) set_tests_properties(guest_savedata_delete PROPERTIES ENVIRONMENT_MODIFICATION From 37ba4a9d2ff1b43568ed324bfd953d1ab2fd5c51 Mon Sep 17 00:00:00 2001 From: valenciana-nc Date: Wed, 21 Oct 2026 16:58:52 -0600 Subject: [PATCH 398/428] fix(libkernel): reject ended threads in pthread_attr_get_np --- .../libs/prx/libkernel/Pthread/Posix/Attr.cpp | 2 ++ core/libs/tests/GuestPthreadAttr.cpp | 27 ++++++++++++++++--- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp index a71ae71597..ddf91b07d3 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp @@ -24,6 +24,8 @@ int APS5_VABI pthread_attr_destroy_nid_postfix(PthreadAttr* attr) { int APS5_VABI pthread_attr_get_np_nid_postfix(Pthread thread, PthreadAttr* attr) { if (!thread || !Valid(attr)) return PosixThread::GUEST_EINVAL; + const auto live = LockLiveGuestThread(thread); + if (!live) return PosixThread::GUEST_ESRCH; auto* p = *attr; p->_stacksize = thread->stackSize; p->stackAddress = thread->stackAddress; diff --git a/core/libs/tests/GuestPthreadAttr.cpp b/core/libs/tests/GuestPthreadAttr.cpp index 5e905c13e9..7e85c80984 100644 --- a/core/libs/tests/GuestPthreadAttr.cpp +++ b/core/libs/tests/GuestPthreadAttr.cpp @@ -1,8 +1,10 @@ #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/Api.hpp" #include "SceTypes.hpp" #include #include +#include #include #include #include @@ -19,6 +21,7 @@ static constexpr KernelCpumask CREATION_AFFINITY = 0x3; static constexpr KernelCpumask UPDATED_AFFINITY = 0x1000; static constexpr std::size_t STACK_SIZE = 2u << 20; static constexpr std::size_t MIN_STACK_SIZE = 16384; +static constexpr int GUEST_ESRCH = 3; static constexpr int GUEST_EINVAL = 22; static constexpr int GUEST_ENOTSUP = 45; static constexpr int DETACH_DETACHED = 1; @@ -225,11 +228,13 @@ static void CheckThreadScope() { Require(scePthreadJoin(inheritedThread, nullptr) == SCE_OK); } -static int QueryResult(Pthread thread) { +static int QueryResult(Pthread thread, bool posix = false) { PthreadAttr attr = nullptr; Require(scePthreadAttrInit(&attr) == SCE_OK); Require(scePthreadAttrSetstacksize(&attr, STACK_SIZE) == SCE_OK); - const int result = scePthreadAttrGet(thread, &attr); + *__error_nid_postfix() = 12345; + const int result = posix ? pthread_attr_get_np_nid_postfix(thread, &attr) : scePthreadAttrGet(thread, &attr); + if (posix) Require(*__error_nid_postfix() == 12345); std::size_t stackSize = 0; Require(scePthreadAttrGetstacksize(&attr, &stackSize) == SCE_OK); Require(result == SCE_OK || stackSize == STACK_SIZE); @@ -237,6 +242,13 @@ static int QueryResult(Pthread thread) { return result; } +static void CheckPosixQuery(Pthread thread, int expected) { + const int result = QueryResult(thread, true); + if (result == expected) return; + std::fprintf(stderr, "pthread_attr_get_np returned %d, expected %d\n", result, expected); + std::abort(); +} + static bool ReportedEnded(Pthread thread) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); while (QueryResult(thread) == SCE_OK) { @@ -249,11 +261,13 @@ static bool ReportedEnded(Pthread thread) { struct ExitQuery { PthreadKey key = -1; int result = -1; + int posixResult = -1; }; static void APS5_VABI QuerySelfAtExit(void* value) { auto* query = static_cast(value); query->result = QueryResult(scePthreadSelf()); + query->posixResult = QueryResult(scePthreadSelf(), true); } static void* APS5_VABI SetExitQuery(void* arg) { @@ -268,22 +282,28 @@ static void* APS5_VABI Finish(void*) { static void CheckEndedThreads() { Require(QueryResult(scePthreadSelf()) == SCE_OK); + CheckPosixQuery(scePthreadSelf(), 0); + CheckPosixQuery(nullptr, GUEST_EINVAL); Pthread joined = nullptr; Require(scePthreadCreate(&joined, nullptr, Finish, nullptr, nullptr) == SCE_OK); Require(scePthreadJoin(joined, nullptr) == SCE_OK); Require(QueryResult(joined) == SCE_KERNEL_ERROR_ESRCH); + CheckPosixQuery(joined, GUEST_ESRCH); PthreadAttr unset = nullptr; Require(scePthreadAttrGet(joined, nullptr) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadAttrGet(joined, &unset) == SCE_KERNEL_ERROR_EINVAL); + Require(pthread_attr_get_np_nid_postfix(joined, nullptr) == GUEST_EINVAL); + Require(pthread_attr_get_np_nid_postfix(joined, &unset) == GUEST_EINVAL); ExitQuery exitQuery; Require(scePthreadKeyCreate(&exitQuery.key, reinterpret_cast(QuerySelfAtExit)) == SCE_OK); Pthread finished = nullptr; Require(scePthreadCreate(&finished, nullptr, SetExitQuery, &exitQuery, nullptr) == SCE_OK); Require(ReportedEnded(finished)); + CheckPosixQuery(finished, GUEST_ESRCH); Require(scePthreadJoin(finished, nullptr) == SCE_OK); - Require(exitQuery.result == SCE_OK); + Require(exitQuery.result == SCE_OK && exitQuery.posixResult == 0); Require(QueryResult(finished) == SCE_KERNEL_ERROR_ESRCH); Require(scePthreadKeyDelete(exitQuery.key) == SCE_OK); @@ -294,6 +314,7 @@ static void CheckEndedThreads() { Pthread detached = nullptr; Require(scePthreadCreate(&detached, &detachedAttr, Finish, nullptr, nullptr) == SCE_OK); Require(ReportedEnded(detached)); + CheckPosixQuery(detached, GUEST_ESRCH); } Require(scePthreadAttrDestroy(&detachedAttr) == SCE_OK); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 4e1851fb4e..f0cc19240b 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -174,7 +174,7 @@ - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), the layout homebrew built against FreeBSD's headers passes: PPSA99039's OpenSSL `ASYNC` (`crypto/async/arch/async_posix.c`) captures into a 0x370-byte stack buffer. `getcontext` writes the 0x330 bytes before `uc_link`, as FreeBSD's `UC_COPY_SIZE`. The console's own layout for these functions is not verified; the exception handler's context is a different one (see `sceKernelRaiseException`) - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) keep the 512-byte FXSAVE image in `mc_fpstate` (x87, MXCSR, XMM0-15), so the upper halves of YMM0-15 are neither saved nor restored, and `getcontext` leaves `mc_xfpustate` and `mc_xfpustate_len` 0. `getcontext` sets `mc_fpformat` and `mc_ownedfp` to FreeBSD's `_MC_FPFMT_XMM` and `_MC_FPOWNED_FPU`, `mc_cs` and `mc_ss` to the host selectors, and `mc_flags`, `mc_fsbase` and `mc_gsbase` to 0. `setcontext` loads `mc_fpstate` whatever `mc_fpformat` and `mc_ownedfp` say, and restores neither the segment selectors, `mc_fsbase` and `mc_gsbase` nor the extended state, which FreeBSD restores when `mc_flags` asks for them - [setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) on Windows set the thread's stack bounds in the TEB to the stack they switch to: a `makecontext` stack, or else the committed region around the target stack pointer. A switch to any other guest stack, such as a context built by hand, gets the bounds of that region only -- [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - the answer for a thread that has ended is not measured on the console, and [`pthread_attr_get_np`](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) still returns 0 for such a thread, reading a detached one's object after it is freed. `scePthreadAttrGet` returns `ESRCH` (`0x80020003`) for a thread that has ended, joined or not and detached or not, as FreeBSD 11.4's [`_pthread_attr_get_np`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L139-L172) does through `_thr_find_thread`; a thread counts as ended once its thread-specific data destructors have run, where FreeBSD marks it `PS_DEAD`. On Windows a host thread adopted by `scePthreadSelf` that ends without `scePthreadExit` keeps answering 0. Monster Hunter Wilds (PPSA24191) reuses a slot of its thread table only when `scePthreadAttrGet` returns `0x80020003` +- [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) and [pthread_attr_get_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - the answer for a thread that has ended is not measured on the console; the implementation follows FreeBSD 11.4's [`_pthread_attr_get_np`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L139-L172) through `_thr_find_thread`. On Windows a host thread adopted by `scePthreadSelf` that ends without `scePthreadExit` keeps answering 0. Monster Hunter Wilds (PPSA24191) reuses a slot of its thread table only when `scePthreadAttrGet` returns `0x80020003` - [scePthreadAttrGet, pthread_attr_get_np and sceKernelIsStack](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) report the stack of a host thread the library did not create, on Windows, from one allocation granule (64 KiB) above the bottom of its reservation when everything above that is committed read-write, as in a relinked executable's main thread and its default-size host threads (0x200000 committed of 0x210000 reserved), so the guest can protect the lowest reported pages, as HotSpot does for its stack guard pages in Java 25 LAN / DNS (PPSA99869). Any other such thread, and every such thread on Linux, gets its whole host stack: on Windows `mprotect` commits its reserved lower pages before protecting them and drops the guard flag of Windows' guard pages, and on Linux it cannot change the unmapped lower pages - [sceKernelInstallExceptionHandler](../../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler, but a host fault still ends the process: only signals raised with `sceKernelRaiseException` are delivered. Unity's PS5Util module installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Linux sends the host SIGUSR1, which the library takes over, to the target thread, so the handler runs wherever the thread was interrupted, emulator code included. On the console kernel objects are system calls and libthr defers signals while a thread holds its internal locks; here a handler that needs a host lock the interrupted code holds deadlocks, and a thread that the handler keeps waiting keeps its host locks. A thread holding the address-wait lock (`sceKernelSyncOnAddressWait`, `sceKernelSyncOnAddressWake`, `_umtx_op`) records the signal and runs the handler once it releases the lock or while it waits; other emulator locks are not deferred. Host waits resume after the handler. The host calls Linux does not restart end early: sleeps that do not retry return before their time, and the poll of `sceNetEpollWait` and socket calls with a send or receive timeout return `EINTR` to the guest From bd31542d500a859719f91aba81d11e90aeaadf65 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 21 Oct 2026 19:03:12 -0400 Subject: [PATCH 399/428] fix(ajm): bound private batch job reads before execution --- core/libs/prx/libSceAjm.native/src/Ajm.cpp | 14 +++-- core/libs/tests/GuestAjm.cpp | 64 ++++++++++++++++++++++ docs/dev/debt/libSceAjm.native.md | 2 + 3 files changed, 76 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 7661f2477d..195ee36a3a 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -1893,14 +1893,19 @@ int APS5_VABI sceAjmBatchJobGetStatistics(AjmBatchInfo* info, float interval, vo int APS5_VABI sceAjmBatchStart(uint32_t context, const AjmBatchInfo* info, int priority, AjmBatchError* error, uint32_t* batch) { if (context == 0 || context >= g_nextContext.load(std::memory_order_relaxed)) return SCE_AJM_ERROR_INVALID_CONTEXT; (void)priority; - if (!info || !batch) return SCE_AJM_ERROR_INVALID_PARAMETER; + if (!info || !batch || !info->p_buffer || info->offset > info->size) return SCE_AJM_ERROR_INVALID_PARAMETER; AJM_TRACE("[ajm] batch start: context %u, priority %d, %llu of %llu buffer bytes used\n", context, priority, static_cast(info->offset), static_cast(info->size)); const auto* cursor = static_cast(info->p_buffer); - const auto* end = cursor + info->offset; + auto remaining = info->offset; std::vector buffers; - while (cursor < end) { + while (remaining != 0) { + if (remaining < sizeof(PackedJob)) NotImplemented_nid_no_patch("AJM batch contains a truncated private job header"); PackedJob record; std::memcpy(&record, cursor, sizeof(record)); + const std::size_t bufferCount = static_cast(record.inputCount) + record.outputCount; + const std::size_t parameterBytes = (static_cast(record.parameterSize) + 7u) & ~std::size_t{7}; + const std::size_t requiredBytes = sizeof(PackedJob) + bufferCount * sizeof(AjmBuffer) + parameterBytes; + if (record.parameterSize > sizeof(JobHeader::parameters) || record.bytes < requiredBytes || record.bytes > remaining) NotImplemented_nid_no_patch("AJM batch contains an invalid private job size"); JobHeader job{}; job.kind = static_cast(record.kind); job.bytes = record.bytes; @@ -1915,7 +1920,8 @@ int APS5_VABI sceAjmBatchStart(uint32_t context, const AjmBatchInfo* info, int p if (!buffers.empty()) std::memcpy(buffers.data(), cursor + sizeof(record), buffers.size() * sizeof(AjmBuffer)); if (record.parameterSize) std::memcpy(job.parameters, cursor + sizeof(record) + buffers.size() * sizeof(AjmBuffer), record.parameterSize); Execute(job, buffers.data(), buffers.data() + job.inputCount); - cursor += record.bytes; + remaining -= record.bytes; + if (remaining != 0) cursor += record.bytes; } if (error) std::memset(error, 0, sizeof(*error)); const auto id = g_nextBatch.fetch_add(1, std::memory_order_relaxed); diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index c7780f5f65..6032301a75 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -841,6 +841,69 @@ void TestGetInfo(std::uint32_t context) { Require(sceAjmInstanceDestroy(context, instance) == 0); } +void TestBatchBounds(std::uint32_t context) { + constexpr int invalidParameter = static_cast(0x80930005); + std::vector storage(128); + AjmBatchInfo info{}; + Require(sceAjmBatchInitialize(storage.data(), storage.size(), &info) == 0); + const std::uint32_t parameters[3] = {2, 48000, 0}; + std::int32_t result[2] = {-1, -1}; + Require(sceAjmBatchJobInitialize(&info, 0, parameters, sizeof(parameters), result) == 0); + const auto completeBytes = info.offset; +#ifdef _WIN32 + SYSTEM_INFO system{}; + GetSystemInfo(&system); + const std::size_t pageSize = system.dwPageSize; + auto* pages = static_cast(VirtualAlloc(nullptr, 2 * pageSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(pages != nullptr); + DWORD oldProtection = 0; + Require(VirtualProtect(pages + pageSize, pageSize, PAGE_NOACCESS, &oldProtection) != 0); +#else + const long hostPageSize = sysconf(_SC_PAGESIZE); + Require(hostPageSize > 0); + const std::size_t pageSize = static_cast(hostPageSize); + auto* pages = static_cast(mmap(nullptr, 2 * pageSize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(pages != MAP_FAILED); + Require(mprotect(pages + pageSize, pageSize, PROT_NONE) == 0); +#endif + Require(completeBytes < pageSize); + for (std::size_t bytes = 1; bytes < completeBytes; ++bytes) { + auto* truncated = pages + pageSize - bytes; + std::memcpy(truncated, storage.data(), bytes); + AjmBatchInfo partial{truncated, bytes, bytes}; + std::uint32_t batch = 0xdeadbeef; + bool threw = false; + try { + sceAjmBatchStart(context, &partial, 0, nullptr, &batch); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && batch == 0xdeadbeef && result[0] == -1 && result[1] == -1); + } + for (std::uint8_t value : {std::uint8_t{0}, std::uint8_t{0xff}}) { + std::fill(storage.begin(), storage.end(), value); + std::uint32_t batch = 0xdeadbeef; + bool threw = false; + try { + sceAjmBatchStart(context, &info, 0, nullptr, &batch); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && batch == 0xdeadbeef && result[0] == -1 && result[1] == -1); + } + std::uint32_t batch = 0xdeadbeef; + AjmBatchInfo invalid{pages + pageSize, 1, 0}; + Require(sceAjmBatchStart(context, &invalid, 0, nullptr, &batch) == invalidParameter); + invalid = {nullptr, 1, 1}; + Require(sceAjmBatchStart(context, &invalid, 0, nullptr, &batch) == invalidParameter); + Require(batch == 0xdeadbeef); +#ifdef _WIN32 + Require(VirtualFree(pages, 0, MEM_RELEASE) != 0); +#else + Require(munmap(pages, 2 * pageSize) == 0); +#endif +} + void TestBatchWaitRelease(std::uint32_t context) { constexpr int invalidBatch = static_cast(0x80930004); std::vector batch(64); @@ -2003,6 +2066,7 @@ int main() { TestGaplessDecode(context); TestCodecInfo(context); TestGetInfo(context); + TestBatchBounds(context); TestBatchWaitRelease(context); TestInstanceHandleLifetime(context); TestBatchCancel(context); diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index fa1c55df74..bdfaa68614 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -27,3 +27,5 @@ - [AJM M4AAC decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 2) - Ghost of Yotei (PPSA26344) initializes it through `sceAjmBatchJobControl`. The initialize parameters (`u32` config type, 1 ADTS or 2 raw, then `u32` sampling frequency index), instance flags (channels in bits 3-6, `ENABLE_SBR_DECODE` bit 32, `ENABLE_NONDELAY_OUTPUT` bit 33, surround interleave order bits 34-35), `{heaac, reserved}` codec info and decoder error result (`CODEC_ERROR | FATAL`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/ajm/ajm_aac.cpp) (PS4, fdk-aac) and are unverified on PS5; decoding uses FFmpeg's AAC decoder. Without `ENABLE_NONDELAY_OUTPUT`, the first two frames after initialization or reset are consumed without output. Raw input takes its channel configuration from the instance (0 means stereo) and is decoded as one access unit per run job. The format reports the last frame's bitrate. Invalid config type or raw sampling frequency index above 11 returns invalid-parameter result 4. The implementation throws for ADTS without a frame header, profiles other than AAC LC, multiple raw data blocks per ADTS frame, unsupported channel configurations, more channels than the instance supports, HE-AAC without `ENABLE_SBR_DECODE`, raw input with `RUN_MULTIPLE_FRAMES`, output room smaller than the previous frame length, and unknown codec flags. - [AJM](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp), [APR](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) and [NGS2](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) state is allocated with `new` and never freed on purpose: title threads still call into these libraries while `std::exit` runs static destructors, so ordinary statics would be destroyed under them. Keep them leaked; `guest_ajm_teardown` covers the AJM case on Unix. - [AJM Opus mapping family 255 with several channels](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): the initialization words carry no stream count or channel mapping, so the decoder opens on the first packet. It counts the self-delimited streams of that multistream packet ([RFC 6716 appendix B](https://datatracker.ietf.org/doc/html/rfc6716#appendix-B)) and takes each stream's stereo flag from its TOC byte as coupled, then decodes with an identity channel mapping. A packet whose streams do not add up to the channel count throws. The layout is inferred, not read from the console; unverified on hardware. + +- [sceAjmBatchStart](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp): console results for malformed batch descriptors and corrupted jobs are unverified. A null batch buffer or an offset beyond its capacity returns the existing invalid-parameter error; a truncated or inconsistent private packed job throws before its payload is read. The packed representation belongs to this implementation. From 949a1e47abb866306f6ac7066064b89d352b59f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 25 Oct 2026 15:53:00 +0100 Subject: [PATCH 400/428] docs(ajm): move the batch bounds debt line inside the Functional section Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceAjm.native.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index bdfaa68614..8c0a9695c5 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -22,10 +22,9 @@ - [sceAjmDecWVorbisCreateHeaderPacket](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjmWVorbis, implemented in libSceAjm.native) - refuses an output smaller than the whole packet (56 bytes plus the setup packet) with `0x8A6E0003`. The PS5 12.00 bit writer only checks that each write starts inside the buffer, so the console can accept a buffer a few bytes short and write past it - [AJM batch contexts](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): start, wait and cancel reject context IDs not allocated by initialization; batches belong to their creating context, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0ebea15c24253b60f514fc37a348dabc8d0bcb90/src/core/libraries/ajm/ajm.cpp). This is not verified on PS5. Finalization still does not invalidate contexts or release their instances and outstanding batches; jobs execute synchronously and cancellation does not interrupt them. +- [sceAjmBatchStart](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp): console results for malformed batch descriptors and corrupted jobs are unverified. A null batch buffer or an offset beyond its capacity returns the existing invalid-parameter error; a truncated or inconsistent private packed job throws before its payload is read. The packed representation belongs to this implementation. - [ATRAC9 gapless segments](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a run job stops after the gapless total of output samples counted from the start of the segment, leaving the rest of its input unconsumed, and the next run starts the next segment with the decoder as it is and without a second skip. FMOD in PPSA29141 sets a total shorter than a sound's frames and reads the frames after it on later runs; with the output held at the total its stream reader waited for them forever and the last buffer repeated. The console's handling of the frames after the total is not documented; a reset (`SetGaplessDecode` with the reset flag, `ClearContext`, initialization) starts a new segment. An ATRAC9 run job without `RUN_MULTIPLE_FRAMES` decodes its whole input as one with it: FMOD in PPSA29141 puts two run jobs in one batch with the second's input right after the whole of the first's. Whether the console stops after one frame there is not verified; MP3 and Opus run jobs still stop after one frame - [AJM M4AAC decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 2) - Ghost of Yotei (PPSA26344) initializes it through `sceAjmBatchJobControl`. The initialize parameters (`u32` config type, 1 ADTS or 2 raw, then `u32` sampling frequency index), instance flags (channels in bits 3-6, `ENABLE_SBR_DECODE` bit 32, `ENABLE_NONDELAY_OUTPUT` bit 33, surround interleave order bits 34-35), `{heaac, reserved}` codec info and decoder error result (`CODEC_ERROR | FATAL`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/ajm/ajm_aac.cpp) (PS4, fdk-aac) and are unverified on PS5; decoding uses FFmpeg's AAC decoder. Without `ENABLE_NONDELAY_OUTPUT`, the first two frames after initialization or reset are consumed without output. Raw input takes its channel configuration from the instance (0 means stereo) and is decoded as one access unit per run job. The format reports the last frame's bitrate. Invalid config type or raw sampling frequency index above 11 returns invalid-parameter result 4. The implementation throws for ADTS without a frame header, profiles other than AAC LC, multiple raw data blocks per ADTS frame, unsupported channel configurations, more channels than the instance supports, HE-AAC without `ENABLE_SBR_DECODE`, raw input with `RUN_MULTIPLE_FRAMES`, output room smaller than the previous frame length, and unknown codec flags. - [AJM](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp), [APR](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) and [NGS2](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) state is allocated with `new` and never freed on purpose: title threads still call into these libraries while `std::exit` runs static destructors, so ordinary statics would be destroyed under them. Keep them leaked; `guest_ajm_teardown` covers the AJM case on Unix. - [AJM Opus mapping family 255 with several channels](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): the initialization words carry no stream count or channel mapping, so the decoder opens on the first packet. It counts the self-delimited streams of that multistream packet ([RFC 6716 appendix B](https://datatracker.ietf.org/doc/html/rfc6716#appendix-B)) and takes each stream's stereo flag from its TOC byte as coupled, then decodes with an identity channel mapping. A packet whose streams do not add up to the channel count throws. The layout is inferred, not read from the console; unverified on hardware. - -- [sceAjmBatchStart](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp): console results for malformed batch descriptors and corrupted jobs are unverified. A null batch buffer or an offset beyond its capacity returns the existing invalid-parameter error; a truncated or inconsistent private packed job throws before its payload is read. The packed representation belongs to this implementation. From 3db438f9979fa84202fb563a6bbbf958265571af Mon Sep 17 00:00:00 2001 From: Itamar Medyoni Date: Wed, 21 Oct 2026 23:55:54 +0300 Subject: [PATCH 401/428] fix(libkernel): answer process CPU clocks like FreeBSD instead of throwing A clock id with CPUCLOCK_PROCESS_BIT threw in clock_gettime and clock_getres. FreeBSD 11 get_cputime reads the process's CPU time, or fails with EINVAL when pget finds no such process, and kern_clock_getres gives every CPU clock the CPU time resolution without a lookup. The guest's own process clock now reads CLOCK_PROCESS_CPUTIME_ID, any other fails with EINVAL, as kill already finds no other process. --- core/libs/prx/libkernel/Time/Time.cpp | 13 +++++------ core/libs/tests/GuestThreadCpuClock.cpp | 29 +++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 4e3d97ebca..79d51858cf 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -44,14 +44,10 @@ static constexpr int GUEST_EFAULT = 14; static constexpr int GUEST_EINVAL = 22; static constexpr int GUEST_CLOCK_REALTIME = 0; static constexpr int GUEST_CLOCK_THREAD_CPUTIME_ID = 14; +static constexpr int GUEST_CLOCK_PROCESS_CPUTIME_ID = 15; static bool IsCpuClock(int clockId) { - const auto bits = static_cast(clockId); - if ((bits & CPU_CLOCK_BIT) == 0) - return false; - if ((bits & CPU_CLOCK_PROCESS_BIT) != 0) - throw std::runtime_error("clock: unsupported process CPU clock_id " + std::to_string(clockId)); - return true; + return (static_cast(clockId) & CPU_CLOCK_BIT) != 0; } static std::uint64_t RawMonotonicNanos() { @@ -391,8 +387,11 @@ static bool ProcessCpuResolutionNanos(std::uint64_t* nanos) { static int ReadClock(int clockId, KernelTimespec* tp) { if (IsCpuClock(clockId)) { + const int id = static_cast(static_cast(clockId) & CPU_CLOCK_ID_MASK); + const bool process = (static_cast(clockId) & CPU_CLOCK_PROCESS_BIT) != 0; + if (process && id == getpid_nid_postfix()) return ReadClock(GUEST_CLOCK_PROCESS_CPUTIME_ID, tp); std::uint64_t nanos = 0; - if (!GuestThreadCpuNanos(static_cast(static_cast(clockId) & CPU_CLOCK_ID_MASK), &nanos)) { + if (process || !GuestThreadCpuNanos(id, &nanos)) { *__error_nid_postfix() = GUEST_EINVAL; return -1; } diff --git a/core/libs/tests/GuestThreadCpuClock.cpp b/core/libs/tests/GuestThreadCpuClock.cpp index 5d5aabcff7..952c0652ea 100644 --- a/core/libs/tests/GuestThreadCpuClock.cpp +++ b/core/libs/tests/GuestThreadCpuClock.cpp @@ -7,12 +7,15 @@ #include #include #include +#include #include static constexpr int GUEST_EFAULT = 14; static constexpr int GUEST_EINVAL = 22; static constexpr int GUEST_CLOCK_MONOTONIC = 4; static constexpr int GUEST_CLOCK_THREAD_CPUTIME_ID = 14; +static constexpr int GUEST_CLOCK_PROCESS_CPUTIME_ID = 15; +static constexpr std::uint32_t PROCESS_CPU_CLOCK = 0xC0000000u; static constexpr std::int64_t NANOS_PER_SECOND = 1000000000LL; static constexpr std::int64_t NANOS_PER_MILLISECOND = 1000000LL; static constexpr std::int64_t BURN_NANOS = 200 * NANOS_PER_MILLISECOND; @@ -99,10 +102,36 @@ static void OtherThreadClockReadsThatThread() { Require(*__error_nid_postfix() == GUEST_EINVAL); } +static void ProcessClocksNameOnlyTheGuest() { + const int self = getpid_nid_postfix(); + const int own = static_cast(PROCESS_CPU_CLOCK | static_cast(self)); + const int other = static_cast(PROCESS_CPU_CLOCK | static_cast(self + 1)); + const std::int64_t start = Nanos(own); + BurnOwnCpu(BURN_NANOS); + const std::int64_t before = Nanos(own); + const std::int64_t reference = Nanos(GUEST_CLOCK_PROCESS_CPUTIME_ID); + const std::int64_t after = Nanos(own); + Require(before - start >= BURN_NANOS - TICK_MARGIN_NANOS); + Require(before <= reference + TICK_MARGIN_NANOS && reference <= after + TICK_MARGIN_NANOS); + KernelTimespec time{-1, -1}; + *__error_nid_postfix() = 0; + Require(clock_gettime_nid_postfix(other, &time) == -1 && *__error_nid_postfix() == GUEST_EINVAL); + Require(time.tv_sec == -1 && time.tv_nsec == -1); + Require(sceKernelClockGettime(other, &time) == SCE_KERNEL_ERROR_EINVAL); + KernelTimespec threadResolution{-1, -1}; + Require(clock_getres_nid_postfix(GUEST_CLOCK_THREAD_CPUTIME_ID, &threadResolution) == 0); + for (const int clockId : {own, other}) { + KernelTimespec resolution{-1, -1}; + Require(clock_getres_nid_postfix(clockId, &resolution) == 0); + Require(resolution.tv_sec == threadResolution.tv_sec && resolution.tv_nsec == threadResolution.tv_nsec); + } +} + int main() { int clockId = 0; Require(pthread_getcpuclockid_nid_postfix(nullptr, &clockId) == GUEST_EINVAL); Require(pthread_getcpuclockid_nid_postfix(pthread_self_nid_postfix(), nullptr) == GUEST_EFAULT); OwnClockFollowsThreadCpuTime(); OtherThreadClockReadsThatThread(); + ProcessClocksNameOnlyTheGuest(); } From ca34fdb3518cb292d166ad43d01efa252cb3a855 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Thu, 22 Oct 2026 00:54:49 +0000 Subject: [PATCH 402/428] feat(libSceNpTrophy2,libSceNpUniversalDataSystem): unlock progressive trophies through _UpdateTrophyProgress Titles report a progressive trophy's progress with _UpdateTrophyProgress events. Their udsNN.ucp rules feed $._trophy_progress into an int32 statistic with the max aggregation, filtered on $._trophy_id, and the trophy's unlockCondition names that statistic. Those statistics and rules were skipped and the events were dropped, so such trophies never reported progress or unlocked (21 of Borderlands 4's 64). - Read max statistics (largest value posted, from initialValue) and int32 statistics (0 to 2147483647). - Read a rule condition with one == property equal to an unsigned integer, and apply the rule only to events whose property matches. - Post _UpdateTrophyProgress through the statistic rules. Co-Authored-By: Claude Opus 5.5 --- .../libSceNpTrophy2/include/TrophyPackage.hpp | 12 +++- .../libSceNpTrophy2/include/TrophyStore.hpp | 1 + .../prx/libSceNpTrophy2/src/TitleTrophies.cpp | 6 ++ .../prx/libSceNpTrophy2/src/TrophyPackage.cpp | 48 +++++++++++--- .../prx/libSceNpTrophy2/src/TrophyStore.cpp | 12 ++++ .../libSceNpTrophy2/tests/TrophyPackage.cpp | 66 ++++++++++++++++--- .../libSceNpUniversalDataSystem/src/Event.cpp | 13 +--- core/libs/tests/GuestNpTrophyStats.cpp | 44 ++++++++++++- docs/dev/debt/libSceNpTrophy2.md | 1 + docs/dev/debt/libSceNpUniversalDataSystem.md | 4 +- docs/user/USAGE.md | 2 +- 11 files changed, 174 insertions(+), 35 deletions(-) diff --git a/core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp b/core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp index 4be4238929..f493e67ea1 100644 --- a/core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp +++ b/core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp @@ -89,17 +89,27 @@ struct TrophyPackage { TrophyPackage ParseTrophyPackage(std::string_view ucp, std::string_view language); -enum class StatType { UInt32, UInt64 }; +enum class StatType { UInt32, UInt64, Int32 }; + +enum class StatAggregation { Latest, Max }; struct StatDefinition { StatType type = StatType::UInt64; + StatAggregation aggregation = StatAggregation::Latest; std::uint64_t minimum = 0; std::uint64_t maximum = UINT64_MAX; + std::uint64_t initial = 0; +}; + +struct StatFilter { + std::string path; + std::uint64_t value = 0; }; struct StatRule { std::string input; std::uint64_t statId = 0; + std::optional filter; }; struct StatRules { diff --git a/core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp b/core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp index d9ab341ae0..c69e678ba3 100644 --- a/core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp +++ b/core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp @@ -67,6 +67,7 @@ public: explicit StatStore(std::filesystem::path directory); void Set(std::int32_t userId, std::uint32_t serviceLabel, std::uint64_t statId, std::uint64_t value); + std::uint64_t Raise(std::int32_t userId, std::uint32_t serviceLabel, std::uint64_t statId, std::uint64_t value); std::optional Value(std::int32_t userId, std::uint32_t serviceLabel, std::uint64_t statId); std::map Values(std::int32_t userId, std::uint32_t serviceLabel); std::filesystem::path RecordPath(std::int32_t userId, std::uint32_t serviceLabel) const; diff --git a/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp b/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp index 813fa576f7..91fb5e816e 100644 --- a/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp @@ -139,6 +139,10 @@ StatChanges ApplyStatRules(const StatRules& rules, const NpTrophy2Event& event, const auto matching = rules.events.find(event.name); if (matching == rules.events.end()) return changes; for (const auto& rule : matching->second) { + if (rule.filter) { + const auto filtered = std::find_if(event.properties.begin(), event.properties.end(), [&rule](const NpTrophy2EventProperty& candidate) { return candidate.path == rule.filter->path; }); + if (filtered == event.properties.end() || filtered->value != rule.filter->value) continue; + } const auto& definition = rules.stats.at(rule.statId); const auto property = std::find_if(event.properties.begin(), event.properties.end(), [&rule](const NpTrophy2EventProperty& candidate) { return candidate.path == rule.input; }); const auto where = event.name + " $." + rule.input + " for stat " + std::to_string(rule.statId); @@ -148,6 +152,8 @@ StatChanges ApplyStatRules(const StatRules& rules, const NpTrophy2Event& event, changes.problems.push_back({where, property->text + " is not an unsigned integer"}); } else if (*property->value < definition.minimum || *property->value > definition.maximum) { changes.problems.push_back({where, property->text + " is not from " + std::to_string(definition.minimum) + " to " + std::to_string(definition.maximum)}); + } else if (definition.aggregation == StatAggregation::Max) { + changes.updates.push_back({rule.statId, stats.Raise(userId, serviceLabel, rule.statId, std::max(*property->value, definition.initial)), rule.input}); } else { stats.Set(userId, serviceLabel, rule.statId, *property->value); changes.updates.push_back({rule.statId, *property->value, rule.input}); diff --git a/core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp b/core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp index 5da39d1cb4..91140de5cf 100644 --- a/core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp @@ -363,26 +363,34 @@ void readStat(const Json& definition, std::size_t index, StatRules& rules) { } const auto name = "stat " + std::to_string(*statId); const auto* aggregation = definition.Find("aggregation"); - if (aggregation == nullptr || aggregation->kind != Json::Kind::String || aggregation->text != "latest") { - rules.skipped.push_back(name + " has aggregation " + (aggregation != nullptr ? describe(*aggregation) : std::string("none")) + "; only \"latest\" is supported"); + const auto named = [](const Json* value, std::string_view text) { return value != nullptr && value->kind == Json::Kind::String && value->text == text; }; + StatDefinition stat; + if (named(aggregation, "latest")) { + stat.aggregation = StatAggregation::Latest; + } else if (named(aggregation, "max")) { + stat.aggregation = StatAggregation::Max; + } else { + rules.skipped.push_back(name + " has aggregation " + (aggregation != nullptr ? describe(*aggregation) : std::string("none")) + "; only \"latest\" and \"max\" are supported"); return; } const auto* type = definition.Find("dataType"); - StatDefinition stat; - if (type != nullptr && type->kind == Json::Kind::String && type->text == "uint32") { + if (named(type, "uint32")) { stat.type = StatType::UInt32; stat.maximum = UINT32_MAX; - } else if (type != nullptr && type->kind == Json::Kind::String && type->text == "uint64") { + } else if (named(type, "uint64")) { stat.type = StatType::UInt64; stat.maximum = UINT64_MAX; + } else if (named(type, "int32")) { + stat.type = StatType::Int32; + stat.maximum = INT32_MAX; } else { - rules.skipped.push_back(name + " has dataType " + (type != nullptr ? describe(*type) : std::string("none")) + "; only \"uint32\" and \"uint64\" are supported"); + rules.skipped.push_back(name + " has dataType " + (type != nullptr ? describe(*type) : std::string("none")) + "; only \"uint32\", \"uint64\" and \"int32\" are supported"); return; } if (const auto* minimum = definition.Find("minValue")) { const auto value = minimum->Unsigned(); if (!value || *value > stat.maximum) { - rules.skipped.push_back(name + " has minValue " + describe(*minimum) + ", which its " + type->text + " type cannot hold"); + rules.skipped.push_back(name + " has minValue " + describe(*minimum) + (stat.type == StatType::Int32 ? ", which is not from 0 to its int32 limit" : ", which its " + type->text + " type cannot hold")); return; } stat.minimum = *value; @@ -395,6 +403,15 @@ void readStat(const Json& definition, std::size_t index, StatRules& rules) { } stat.maximum = *value; } + stat.initial = stat.minimum; + if (const auto* initial = definition.Find("initialValue"); initial != nullptr && stat.aggregation == StatAggregation::Max) { + const auto value = initial->Unsigned(); + if (!value || *value < stat.minimum || *value > stat.maximum) { + rules.skipped.push_back(name + " has initialValue " + describe(*initial) + ", which is not from its minValue to its maxValue"); + return; + } + stat.initial = *value; + } if (!rules.stats.emplace(*statId, stat).second) rules.skipped.push_back(name + " is defined twice; the first definition is used"); } @@ -411,9 +428,20 @@ void readRule(const Json& rule, std::size_t index, StatRules& rules) { rules.skipped.push_back(where + " has no eventName"); return; } + std::optional filter; if (condition->members.size() != 1) { - rules.skipped.push_back(where + " filters " + event->text + " by more than its eventName; only eventName conditions are supported"); - return; + const auto* property = condition->Find("property"); + const auto* path = property != nullptr && property->kind == Json::Kind::Object ? property->Find("path") : nullptr; + const auto* comparator = property != nullptr && property->kind == Json::Kind::Object ? property->Find("comparator") : nullptr; + const auto* value = property != nullptr && property->kind == Json::Kind::Object ? property->Find("value") : nullptr; + const auto filterPath = path != nullptr && path->kind == Json::Kind::String ? propertyPath(path->text) : std::nullopt; + const auto filterValue = value != nullptr ? value->Unsigned() : std::nullopt; + const bool equal = comparator != nullptr && comparator->kind == Json::Kind::String && comparator->text == "=="; + if (condition->members.size() != 2 || property == nullptr || property->kind != Json::Kind::Object || property->members.size() != 3 || !filterPath || !equal || !filterValue) { + rules.skipped.push_back(where + " filters " + event->text + " by more than its eventName; only an eventName with one \"$.name\" property equal to an unsigned integer is supported"); + return; + } + filter = StatFilter{*filterPath, *filterValue}; } const auto* output = action->Find("output"); const auto* id = output != nullptr ? output->Find("statId") : nullptr; @@ -432,7 +460,7 @@ void readRule(const Json& rule, std::size_t index, StatRules& rules) { rules.skipped.push_back(where + " for " + event->text + " reads " + (input != nullptr ? describe(*input) : std::string("no input")) + "; only \"$.name\" property paths are supported"); return; } - rules.events[event->text].push_back({*path, *statId}); + rules.events[event->text].push_back({*path, *statId, std::move(filter)}); } } diff --git a/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp b/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp index b19804fe10..5091aad105 100644 --- a/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp @@ -177,6 +177,18 @@ void StatStore::Set(std::int32_t userId, std::uint32_t serviceLabel, std::uint64 stats = std::move(updated); } +std::uint64_t StatStore::Raise(std::int32_t userId, std::uint32_t serviceLabel, std::uint64_t statId, std::uint64_t value) { + std::lock_guard lock(mutex); + const Key key{userId, serviceLabel}; + auto& stats = record(key); + if (const auto found = stats.find(statId); found != stats.end() && found->second >= value) return found->second; + auto updated = stats; + updated[statId] = value; + writeRecord(directory, RecordPath(userId, serviceLabel), updated, "stat record"); + stats = std::move(updated); + return value; +} + std::optional StatStore::Value(std::int32_t userId, std::uint32_t serviceLabel, std::uint64_t statId) { std::lock_guard lock(mutex); const auto& stats = record({userId, serviceLabel}); diff --git a/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp b/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp index d1dcdba734..0e7b0e4364 100644 --- a/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp +++ b/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp @@ -94,7 +94,11 @@ const char StatsDefinition[] = R"({"statDefinitionArray":[ {"statId":50,"dataType":"uint64","aggregation":"sum"}, {"statId":60,"dataType":"int32","aggregation":"latest"}, {"statId":70,"dataType":"uint32","aggregation":"latest","maxValue":"5000000000"}, - {"dataType":"uint32","aggregation":"latest"} + {"dataType":"uint32","aggregation":"latest"}, + {"statId":80,"dataType":"int32","aggregation":"max","minValue":"0","initialValue":"0"}, + {"statId":90,"dataType":"uint32","aggregation":"max","initialValue":"3"}, + {"statId":100,"dataType":"int32","aggregation":"max","initialValue":"-1"}, + {"statId":110,"dataType":"int32","aggregation":"latest","minValue":"-5"} ]})"; const char StatsExtraction[] = R"({"statsExtractionRuleArray":[ @@ -106,7 +110,12 @@ const char StatsExtraction[] = R"({"statsExtractionRuleArray":[ {"condition":{"eventName":"FILTERED","property":{"path":"$.level"}},"action":{"input":"$.counter","output":{"statId":10}}}, {"condition":{"eventName":"COUNT"},"action":{"output":{"statId":10}}}, {"condition":{"eventName":"BRACKETS"},"action":{"input":"$['counter']","output":{"statId":10}}}, - {"bad":1} + {"bad":1}, + {"condition":{"eventName":"PROGRESS","property":{"path":"$._trophy_id","comparator":"==","value":"5"}},"action":{"input":"$._trophy_progress","output":{"statId":80}}}, + {"condition":{"eventName":"PROGRESS","property":{"path":"$._trophy_id","comparator":"==","value":6}},"action":{"input":"$._trophy_progress","output":{"statId":90}}}, + {"condition":{"eventName":"RANGED","property":{"path":"$._trophy_id","comparator":">=","value":"5"}},"action":{"input":"$._trophy_progress","output":{"statId":80}}}, + {"condition":{"eventName":"NAMED","property":{"path":"$.mode","comparator":"==","value":"delta"}},"action":{"input":"$._trophy_progress","output":{"statId":80}}}, + {"condition":{"eventName":"EXTRA","property":{"path":"$._trophy_id","comparator":"==","value":"5"},"other":1},"action":{"input":"$._trophy_progress","output":{"statId":80}}} ]})"; std::string trophyPackage() { @@ -310,10 +319,13 @@ void testProgressAndPlatinum() { void testStatRules() { const auto rules = NpTrophy2::ParseStatRules(statPackage()); - check(rules.stats.size() == 4 && rules.stats.at(10).type == NpTrophy2::StatType::UInt64 && rules.stats.at(30).type == NpTrophy2::StatType::UInt32 && rules.stats.at(30).maximum == UINT32_MAX && - rules.stats.at(40).maximum == 50 && rules.stats.at(10).minimum == 0, + check(rules.stats.size() == 7 && rules.stats.at(10).type == NpTrophy2::StatType::UInt64 && rules.stats.at(30).type == NpTrophy2::StatType::UInt32 && rules.stats.at(30).maximum == UINT32_MAX && + rules.stats.at(40).maximum == 50 && rules.stats.at(10).minimum == 0 && rules.stats.at(10).aggregation == NpTrophy2::StatAggregation::Latest, "stat definitions were not read"); - check(rules.events.size() == 3 && rules.events.at("COLLECT").size() == 2 && rules.events.at("COLLECT")[1].input == "total" && rules.events.at("COLLECT")[1].statId == 20 && + check(rules.stats.at(60).type == NpTrophy2::StatType::Int32 && rules.stats.at(60).maximum == INT32_MAX && rules.stats.at(80).aggregation == NpTrophy2::StatAggregation::Max && + rules.stats.at(80).type == NpTrophy2::StatType::Int32 && rules.stats.at(80).initial == 0 && rules.stats.at(90).aggregation == NpTrophy2::StatAggregation::Max && rules.stats.at(90).initial == 3, + "int32 and max stat definitions were not read"); + check(rules.events.size() == 4 && rules.events.at("COLLECT").size() == 2 && rules.events.at("COLLECT")[1].input == "total" && rules.events.at("COLLECT")[1].statId == 20 && rules.events.at("STEPS")[0].input == "player.steps" && rules.events.at("TIME")[0].statId == 30, "extraction rules were not read"); std::set skipped(rules.skipped.begin(), rules.skipped.end()); @@ -323,11 +335,17 @@ void testStatRules() { } return false; }; - check(rules.skipped.size() == 9, "unsupported definitions and rules were not all reported"); - check(reported("stat 50 has aggregation \"sum\"") && reported("stat 60 has dataType \"int32\"") && reported("stat 70 has maxValue \"5000000000\"") && reported("statDefinitionArray[7] has no unsigned statId"), + check(rules.skipped.size() == 13, "unsupported definitions and rules were not all reported"); + check(reported("stat 50 has aggregation \"sum\"; only \"latest\" and \"max\" are supported") && reported("stat 70 has maxValue \"5000000000\"") && reported("statDefinitionArray[7] has no unsigned statId") && + reported("stat 100 has initialValue \"-1\"") && reported("stat 110 has minValue \"-5\", which is not from 0 to its int32 limit"), "unsupported definitions were not reported"); - check(reported("for SUM writes stat 50") && reported("filters FILTERED") && reported("for COUNT reads no input") && reported("for BRACKETS reads \"$['counter']\"") && reported("[8] has no condition"), + check(reported("for SUM writes stat 50") && reported("filters FILTERED") && reported("for COUNT reads no input") && reported("for BRACKETS reads \"$['counter']\"") && reported("[8] has no condition") && + reported("filters RANGED") && reported("filters NAMED") && reported("filters EXTRA"), "unsupported rules were not reported"); + const auto& progress = rules.events.at("PROGRESS"); + check(progress.size() == 2 && progress[0].filter && progress[0].filter->path == "_trophy_id" && progress[0].filter->value == 5 && progress[0].input == "_trophy_progress" && progress[0].statId == 80 && + progress[1].filter && progress[1].filter->value == 6 && progress[1].statId == 90 && !rules.events.at("COLLECT")[0].filter, + "property filters were not read"); for (const char* input : {"$", "$.", "$..a", "$.a.", "$.a-b", "counter", "$.a b"}) { const std::string extraction = std::string(R"({"statsExtractionRuleArray":[{"condition":{"eventName":"E"},"action":{"input":")") + input + R"(","output":{"statId":10}}}]})"; check(NpTrophy2::ParseStatRules(ucp({{"stats_definition.json", StatsDefinition}, {"stats_extraction.json", extraction}})).events.empty(), "an unsupported input path was accepted"); @@ -412,6 +430,37 @@ void testApplyStatRules() { check(latest.updates.size() == 2 && stats.Value(InitialUser, 0, 10) == std::optional(2), "a stat did not keep the latest value"); } +void testProgressRules() { + const auto rules = NpTrophy2::ParseStatRules(statPackage()); + NpTrophy2::StatStore stats("progress"); + const auto posted = [&](std::vector properties) { return NpTrophy2::ApplyStatRules(rules, event("PROGRESS", std::move(properties)), stats, InitialUser, 0); }; + const auto progress = [&](std::uint64_t trophy, std::uint64_t value) { return posted({{"_trophy_id", trophy, std::to_string(trophy)}, {"_trophy_progress", value, std::to_string(value)}}); }; + const auto first = progress(5, 4); + check(first.problems.empty() && first.updates.size() == 1 && first.updates[0].statId == 80 && first.updates[0].value == 4 && stats.Value(InitialUser, 0, 80) == std::optional(4) && + !stats.Value(InitialUser, 0, 90), + "a filtered rule did not take its trophy's progress, or another trophy's rule did"); + const auto lower = progress(5, 2); + check(lower.updates.size() == 1 && lower.updates[0].value == 4 && stats.Value(InitialUser, 0, 80) == std::optional(4), "a max stat went down"); + const auto higher = progress(5, 7); + check(higher.updates.size() == 1 && higher.updates[0].value == 7 && stats.Value(InitialUser, 0, 80) == std::optional(7), "a max stat did not rise"); + const auto floor = progress(6, 1); + check(floor.updates.size() == 1 && floor.updates[0].statId == 90 && floor.updates[0].value == 3 && stats.Value(InitialUser, 0, 90) == std::optional(3), "a max stat started below its initialValue"); + const auto signedMax = progress(5, 2147483648ull); + check(signedMax.updates.empty() && signedMax.problems.size() == 1 && signedMax.problems[0].reason == "2147483648 is not from 0 to 2147483647", "an int32 stat took a value it cannot hold"); + const auto other = progress(7, 9); + const auto unnamed = posted({{"_trophy_progress", 9, "9"}}); + const auto negative = posted({{"_trophy_id", std::nullopt, "-5"}, {"_trophy_progress", 9, "9"}}); + check(other.updates.empty() && other.problems.empty() && unnamed.updates.empty() && unnamed.problems.empty() && negative.updates.empty() && negative.problems.empty() && + stats.Value(InitialUser, 0, 80) == std::optional(7) && stats.Value(InitialUser, 0, 90) == std::optional(3), + "a filtered rule took an event for another trophy, or without its trophy id"); + const auto missing = posted({{"_trophy_id", 5, "5"}}); + check(missing.updates.empty() && missing.problems.size() == 1 && missing.problems[0].rule == "PROGRESS $._trophy_progress for stat 80", "a matching event without its input was not reported"); + NpTrophy2::StatStore restarted("progress"); + check(restarted.Raise(InitialUser, 0, 80, 6) == 7 && restarted.Raise(InitialUser, 0, 80, 8) == 8 && restarted.Raise(InitialUser, 0, 120, 1) == 1, "a raise did not keep the larger value"); + NpTrophy2::StatStore reread("progress"); + check(reread.Value(InitialUser, 0, 80) == std::optional(8) && reread.Value(InitialUser, 0, 120) == std::optional(1), "a raised stat was not stored"); +} + } int main() { @@ -427,6 +476,7 @@ int main() { testConditions(); testStatStore(); testApplyStatRules(); + testProgressRules(); std::filesystem::current_path(previous); std::filesystem::remove_all(root); std::puts("Trophy package tests passed"); diff --git a/core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp b/core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp index 942a5f9450..12bcbd69c4 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp +++ b/core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp @@ -5,8 +5,6 @@ #include #include #include -#include -#include #include #include @@ -22,13 +20,6 @@ Uds::Owned& events() { return registry; } -bool firstProgress(std::int32_t trophyId) { - static std::mutex mutex; - static std::set logged; - std::lock_guard lock(mutex); - return logged.insert(trophyId).second; -} - void ignore(const NpUniversalDataSystemEvent& event, int context, const std::string& reason) { std::fprintf(stderr, "[trophy] ignored %s posted to context %d: %s\n", Uds::Describe(event).c_str(), context, reason.c_str()); } @@ -39,14 +30,12 @@ void post(const NpUniversalDataSystemEvent& event, int context) { const auto trophy = Uds::RecognizeTrophyEvent(event); switch (trophy.kind) { case Uds::TrophyEventKind::None: + case Uds::TrophyEventKind::Progress: if (const auto owner = Uds::FindContext(context)) NpTrophy2ApplyEvent_nid_no_patch(owner->userId, owner->serviceLabel, Uds::StatEvent(event)); return; case Uds::TrophyEventKind::Malformed: ignore(event, context, trophy.problem); return; - case Uds::TrophyEventKind::Progress: - if (firstProgress(trophy.trophyId)) ignore(event, context, "trophy progress is not recorded"); - return; case Uds::TrophyEventKind::Unlock: if (const auto owner = Uds::FindContext(context)) NpTrophy2RecordUnlock_nid_no_patch(owner->userId, owner->serviceLabel, trophy.trophyId); else ignore(event, context, "the context does not exist"); diff --git a/core/libs/tests/GuestNpTrophyStats.cpp b/core/libs/tests/GuestNpTrophyStats.cpp index 0128a97160..35511ae5b6 100644 --- a/core/libs/tests/GuestNpTrophyStats.cpp +++ b/core/libs/tests/GuestNpTrophyStats.cpp @@ -45,6 +45,20 @@ static const char StatsDefinition[] = R"({"statDefinitionArray":[ {"statId":10,"dataType":"uint64","aggregation":"latest"},{"statId":20,"dataType":"uint64","aggregation":"latest"}, {"statId":30,"dataType":"uint32","aggregation":"latest"},{"statId":40,"dataType":"uint32","aggregation":"latest"}]})"; +static const char ProgressConf[] = R"({"defaultLanguage":"en-US","trophies":[ + {"id":"0","grade":"P"}, + {"id":"1","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"112","comparator":"ge","targetValue":"10","progressive":true}}, + {"id":"2","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"113","comparator":"ge","targetValue":"3","progressive":true}} +]})"; + +static const char ProgressDefinition[] = R"({"statDefinitionArray":[ + {"statId":112,"dataType":"int32","aggregation":"max","minValue":"0","initialValue":"0"}, + {"statId":113,"dataType":"int32","aggregation":"max","minValue":"0","initialValue":"0"}]})"; + +static const char ProgressExtraction[] = R"({"statsExtractionRuleArray":[ + {"condition":{"eventName":"_UpdateTrophyProgress","property":{"path":"$._trophy_id","comparator":"==","value":"1"}},"action":{"input":"$._trophy_progress","output":{"statId":112}}}, + {"condition":{"eventName":"_UpdateTrophyProgress","property":{"path":"$._trophy_id","comparator":"==","value":"2"}},"action":{"input":"$._trophy_progress","output":{"statId":113}}}]})"; + static const char SumDefinition[] = R"({"statDefinitionArray":[{"statId":10,"dataType":"uint64","aggregation":"sum"}]})"; static const char StatsExtraction[] = R"({"statsExtractionRuleArray":[ @@ -308,11 +322,36 @@ static void IgnoreOtherAggregations(Unlocks& unlocks) { Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "counter", 10) == 0, "counter was not set"); }); const auto log = capture.Finish(); - Require(log.find("[trophy] /app0/sce_sys/uds/uds05.ucp: stat 10 has aggregation \"sum\"; only \"latest\" is supported") != std::string::npos, + Require(log.find("[trophy] /app0/sce_sys/uds/uds05.ucp: stat 10 has aggregation \"sum\"; only \"latest\" and \"max\" are supported") != std::string::npos, "a stat with a sum aggregation was not reported"); Require(!Trophy(context, 1).unlocked && !Exists("_trophy/268435456_5.stats.txt") && unlocks.trophies.size() == 6, "a stat with a sum aggregation was stored or unlocked a trophy"); } +static void RecordTrophyProgress(Unlocks& unlocks) { + const auto context = TrophyContext(6); + const auto uds = OpenUds(6); + const auto progress = [&uds](std::int32_t trophy, std::int32_t value) { + Post(uds, "_UpdateTrophyProgress", [trophy, value](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetInt32(properties, "_trophy_id", trophy) == 0, "_trophy_id was not set"); + Require(sceNpUniversalDataSystemEventPropertyObjectSetInt32(properties, "_trophy_progress", value) == 0, "_trophy_progress was not set"); + }); + }; + progress(1, 4); + const auto partway = Trophy(context, 1); + Require(!partway.unlocked && partway.progress.type == 1 && partway.progress.value == 4 && Trophy(context, 2).progress.value == 0, "trophy progress was not recorded for its own trophy"); + progress(1, 2); + Require(Trophy(context, 1).progress.value == 4, "lower trophy progress replaced higher progress"); + progress(2, 3); + Require(Trophy(context, 2).unlocked && !Trophy(context, 1).unlocked && !Trophy(context, 0).unlocked, "trophy progress at its target did not unlock only that trophy"); + progress(1, 12); + const auto done = Trophy(context, 1); + Require(done.unlocked && done.progress.value == 10 && Trophy(context, 0).unlocked, "trophy progress past its target did not unlock the trophy and the platinum"); + Require(Read("_trophy/268435456_6.stats.txt") == "112 12\n113 3\n", "trophy progress was not stored"); + const std::vector> expected{{6, 2}, {6, 1}, {6, 0}}; + Require(unlocks.trophies.size() == 9 && std::vector>(unlocks.trophies.end() - 3, unlocks.trophies.end()) == expected, + "the listener did not see the progress unlocks and the platinum once, in order"); +} + int main() { const auto previous = std::filesystem::current_path(); const auto root = std::filesystem::temp_directory_path() / ("anyps5-trophy-stats-" + std::to_string(std::random_device{}())); @@ -329,6 +368,8 @@ int main() { Write("app0/sce_sys/uds/uds03.ucp", rules.substr(0, rules.size() - 1)); Write("app0/sce_sys/trophy2/trophy05.ucp", trophies); Write("app0/sce_sys/uds/uds05.ucp", Ucp({{"stats_definition.json", SumDefinition}, {"stats_extraction.json", StatsExtraction}})); + Write("app0/sce_sys/trophy2/trophy06.ucp", Ucp({{"tropconf.json", ProgressConf}, {"tropmeta_en-US.json", TropMeta}})); + Write("app0/sce_sys/uds/uds06.ucp", Ucp({{"stats_definition.json", ProgressDefinition}, {"stats_extraction.json", ProgressExtraction}})); Write("_trophy/268435456_0.stats.txt", "40 2\n"); Unlocks unlocks; @@ -338,6 +379,7 @@ int main() { UnlockThroughStats(context, unlocks); IgnoreMissingAndMalformedFiles(unlocks); IgnoreOtherAggregations(unlocks); + RecordTrophyProgress(unlocks); NpTrophy2RemoveUnlockListener_nid_no_patch(listener); std::filesystem::current_path(previous); diff --git a/docs/dev/debt/libSceNpTrophy2.md b/docs/dev/debt/libSceNpTrophy2.md index 189c214f1d..8552c9e0de 100644 --- a/docs/dev/debt/libSceNpTrophy2.md +++ b/docs/dev/debt/libSceNpTrophy2.md @@ -16,5 +16,6 @@ - [Trophy queries](../../../core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp) - not confirmed on the console: `sceNpTrophy2GetTrophyInfo` of a trophy id that is not in the title's [trophy data](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp) (`sce_sys/trophy2/trophyNN.ucp`) returns `0x8055390A` (KytyPS5's `NP_TROPHY2_ERROR_INVALID_TROPHY_ID`) and writes nothing; a context not created or destroyed reports placeholders and no unlocks and returns success; hidden trophies report their names and descriptions before the unlock; `progress_percentage` weighs gold 6, silver 2 and bronze 1 and leaves the platinum out, as KytyPS5 does (the ratio of the published PSN trophy points). A group that is not in the data throws. Without the file, or when it is malformed, the queries report the old placeholders and `sceNpTrophy2GetTrophyInfoArray` lists only trophy 0. Icons are not read. Unlocks come from the [trophy store](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp). - [Trophy data limits](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): a `trophyNN.ucp` or `udsNN.ucp` larger than 256 MiB or listing more than 4096 files, and a JSON file in it that nests deeper than 64 levels or holds more than 1048576 values, are not used, and the title runs as without that file. The limits keep the memory the reader takes to the file and about 140 MiB of parsed JSON; the console's limits are not known. - [Text language](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): texts come from `tropmeta_.json` for the console language, then `en-US`, then the file's `defaultLanguage`. The tags for languages 0 to 29 are KytyPS5's; `uk-UA` for 30 (Ukrainian) is an assumption. +- [Trophy progress statistics](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): `max` (the largest value posted, starting from `initialValue`, or `minValue` without one), `int32` (0 to 2147483647) and a rule condition of an `eventName` and one `property` whose `comparator` is `==` and `value` an unsigned integer follow the rules a title ships for its progressive trophies, not a console measurement. Borderlands 4 (PPSA01494) feeds each one's `udsStatId` from `$._trophy_progress` of `_UpdateTrophyProgress`, through an `int32` `max` statistic and a rule on `$._trophy_id`. `latest` and the unsigned types follow KytyPS5. - [Platinum](../../../core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp): an unlock through any path unlocks the platinum that the trophy names in `platinumTrophyId` once every trophy naming that platinum is unlocked, as KytyPS5 does; trophies without `platinumTrophyId` are not required. With the title's trophy data loaded, a posted `_UnlockTrophy` of a trophy that is not in the data, of a platinum or of a progressive trophy is not stored and prints a `[trophy] ignored` line, as KytyPS5 refuses them, so a progressive trophy unlocks only through its statistic; without trophy data any id of 0 or more is stored, including ids the data would make the platinum, a progressive trophy or no trophy, and the platinum is not unlocked when the trophies it needs are. The console's behaviour is not known. - [Damaged records](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp): a line in `_trophy/_.txt` or `.stats.txt` that is not a new ` ` pair (a blank line, a carriage return, a repeated or negative id, a value that is not a number) makes `readRecord` throw a `std::runtime_error` that names the file and the line. Every call that reads that record then throws it instead of returning, which stops the title as any throwing export does, so the title gets no return value; main returned 0 from these calls. The failed read is not kept, so each later call reads the file again and throws again until the file is fixed or deleted. The unlock record is read by `sceNpTrophy2GetTrophyInfo`, `sceNpTrophy2GetTrophyInfoArray` and `sceNpTrophy2GetGameInfo` for a created context, by `sceNpTrophy2GetGroupInfo` and `sceNpTrophy2GetGroupInfoArray` when the title's trophy data is loaded, by a `sceNpUniversalDataSystemPostEvent` of an `_UnlockTrophy` that the trophy data does not refuse, and by a statistic change when the trophy data is loaded; the stat record by a `sceNpUniversalDataSystemPostEvent` of an event that a statistics rule copies and by the trophy queries of a progressive trophy with a statistic. What the console does with a damaged record is not known. diff --git a/docs/dev/debt/libSceNpUniversalDataSystem.md b/docs/dev/debt/libSceNpUniversalDataSystem.md index 2e07cb9901..cbc943be5e 100644 --- a/docs/dev/debt/libSceNpUniversalDataSystem.md +++ b/docs/dev/debt/libSceNpUniversalDataSystem.md @@ -19,5 +19,5 @@ ### Functional -- [Trophy events](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): `sceNpUniversalDataSystemPostEvent` stores an `_UnlockTrophy` event with an int32 `_trophy_id` of 0 or more as an unlock for the user and service label of its context, as KytyPS5 does, except an id that [libSceNpTrophy2](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp) refuses when the title's trophy data is loaded (one not in the data, a platinum or a progressive trophy). `_UpdateTrophyProgress`, named only by a third-party unlocker, is logged once per trophy and not interpreted. A context need not be registered, and every post of an event that `sceNpUniversalDataSystemCreateEvent` returned and that is not yet destroyed returns success (a null or any other event pointer stops the game): KytyPS5's errors for an unknown or unregistered context and for a malformed unlock are not verified. The storage statistics stay zero. -- [Statistics](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): every other event goes through the title's rules in `sce_sys/uds/udsNN.ucp`, which [libSceNpTrophy2](../../../core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp) reads. Only statistics with `"aggregation":"latest"` and `dataType` `uint32` or `uint64` are kept, as in KytyPS5; no public source documents the other aggregations or types, so their definitions and rules are skipped. Only rules whose condition is an `eventName` alone and whose input is a `$.name` path (dotted for nested objects) are applied. A value is used when it is an integer of any width that is not negative and fits the statistic's type and `minValue`/`maxValue`; KytyPS5 requires the statistic's exact width, and whether the console converts, clamps or rejects other values is not known. Statistics are stored per user and service label, and every trophy's `unlockCondition` is compared after each event that sets one. +- [Trophy events](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): `sceNpUniversalDataSystemPostEvent` stores an `_UnlockTrophy` event with an int32 `_trophy_id` of 0 or more as an unlock for the user and service label of its context, as KytyPS5 does, except an id that [libSceNpTrophy2](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp) refuses when the title's trophy data is loaded (one not in the data, a platinum or a progressive trophy). `_UpdateTrophyProgress` goes through the title's statistic rules like any other event (below). A context need not be registered, and every post of an event that `sceNpUniversalDataSystemCreateEvent` returned and that is not yet destroyed returns success (a null or any other event pointer stops the game): KytyPS5's errors for an unknown or unregistered context and for a malformed unlock are not verified. The storage statistics stay zero. +- [Statistics](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): every other event goes through the title's rules in `sce_sys/uds/udsNN.ucp`, which [libSceNpTrophy2](../../../core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp) reads. Only `"aggregation":"latest"` and `"max"` statistics of `dataType` `uint32`, `uint64` or `int32` are kept; no public source documents the other aggregations (such as `count`, `collection` and `eachLatest`) or types, so their definitions and rules are skipped. Only rules whose input is a `$.name` path (dotted for nested objects) are applied. A value is used when it is an integer of any width that is not negative and fits the statistic's type and `minValue`/`maxValue`; KytyPS5 requires the statistic's exact width, and whether the console converts, clamps or rejects other values is not known. Statistics are stored per user and service label, and every trophy's `unlockCondition` is compared after each event that sets one. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index cc00fbe7d1..564605ea5a 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -274,7 +274,7 @@ The trophy data comes from the game's own `app0/sce_sys/trophy2/trophyNN.ucp`, w Many games unlock trophies through statistics instead: `app0/sce_sys/uds/udsNN.ucp` names, for each event, the property whose value becomes a statistic, and a trophy's `unlockCondition` compares a statistic with a target. The latest value of each statistic is stored in `_trophy/_.stats.txt`, one ` ` line per statistic, and is kept across runs; delete it together with the unlock file to start over. A progressive trophy reports its statistic as progress. Only statistics that keep the latest value (`"aggregation":"latest"`) as `uint32` or `uint64`, and rules that copy one property of an event chosen by its name (`$.name` or `$.object.name`), are applied; the others are listed in `[trophy]` lines when the file is read. -`APS5_TRACE_UDS=1` prints every posted event, such as `[uds] context 1 posted _UnlockTrophy {"_trophy_id":12}`, every statistic an event sets, such as `[uds] stat 10 of user 268435456 is 10 (COLLECT $.counter)`, and a `[uds]` line for a missing `trophyNN.ucp` or `udsNN.ucp`. An event that is not used prints the reason in a `[trophy] ignored` line: an `_UnlockTrophy` event without an int32 `_trophy_id` of 0 or more, or posted to a context that does not exist; the first `_UpdateTrophyProgress` event of each trophy, because that event is not interpreted; and, once per rule, an event whose property is missing, negative, not an integer or outside the statistic's range. +`APS5_TRACE_UDS=1` prints every posted event, such as `[uds] context 1 posted _UnlockTrophy {"_trophy_id":12}`, every statistic an event sets, such as `[uds] stat 10 of user 268435456 is 10 (COLLECT $.counter)`, and a `[uds]` line for a missing `trophyNN.ucp` or `udsNN.ucp`. An event that is not used prints the reason in a `[trophy] ignored` line: an `_UnlockTrophy` event without an int32 `_trophy_id` of 0 or more, or posted to a context that does not exist; and, once per rule, an event whose property is missing, negative, not an integer or outside the statistic's range. A missing or malformed `trophyNN.ucp` or `udsNN.ucp` turns off only what it provides, and a malformed one prints the reason once: without trophy data the queries report placeholders and statistics unlock nothing, and without statistics rules events change no statistic. Icons are not read: `sceNpTrophy2GetGameIcon`, `sceNpTrophy2GetGroupIcon`, `sceNpTrophy2GetTrophyIcon` and `sceNpTrophy2GetRewardIcon` stop the game with an error. So does an unlock or statistics file in `_trophy/` that is damaged (a line that is not a new ` ` pair, such as a line an edit left blank or ended with a carriage return) or cannot be read or written. No notification is shown, and callbacks registered with `sceNpTrophy2RegisterUnlockCallback` are not called. From 6ce060e3eee304cb452a37889c60c9a13bee134c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 25 Oct 2026 17:24:50 +0100 Subject: [PATCH 403/428] docs(libSceNpTrophy2): describe max and int32 statistics and property filters in USAGE --- docs/user/USAGE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 564605ea5a..4a894c2f27 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -272,7 +272,7 @@ A game unlocks a trophy by posting a `_UnlockTrophy` event with an int32 `_troph The trophy data comes from the game's own `app0/sce_sys/trophy2/trophyNN.ucp`, where `NN` is the service label (`00` for most games). The trophy queries report its names, descriptions and rewards in the console language (`ANYPS5_LANGUAGE`, see above), or in English (United States) or the data's default language when it has no text in that language, and its grades, groups, hidden flags, number of trophies and grade counts. A platinum trophy unlocks as soon as every trophy that names it in `platinumTrophyId` is unlocked. The first trophy query or event prints what was read, such as `[trophy] /app0/sce_sys/trophy2/trophy00.ucp: 52 trophies, 1 group, texts in en-US`. -Many games unlock trophies through statistics instead: `app0/sce_sys/uds/udsNN.ucp` names, for each event, the property whose value becomes a statistic, and a trophy's `unlockCondition` compares a statistic with a target. The latest value of each statistic is stored in `_trophy/_.stats.txt`, one ` ` line per statistic, and is kept across runs; delete it together with the unlock file to start over. A progressive trophy reports its statistic as progress. Only statistics that keep the latest value (`"aggregation":"latest"`) as `uint32` or `uint64`, and rules that copy one property of an event chosen by its name (`$.name` or `$.object.name`), are applied; the others are listed in `[trophy]` lines when the file is read. +Many games unlock trophies through statistics instead: `app0/sce_sys/uds/udsNN.ucp` names, for each event, the property whose value becomes a statistic, and a trophy's `unlockCondition` compares a statistic with a target. The latest value of each statistic is stored in `_trophy/_.stats.txt`, one ` ` line per statistic, and is kept across runs; delete it together with the unlock file to start over. A progressive trophy reports its statistic as progress. Only statistics that keep the latest (`"aggregation":"latest"`) or the largest value (`"max"`) as `uint32`, `uint64` or `int32`, and rules that copy one property of an event chosen by its name (`$.name` or `$.object.name`), optionally only when one property equals an integer (`_UpdateTrophyProgress` by its `_trophy_id`), are applied; the others are listed in `[trophy]` lines when the file is read. `APS5_TRACE_UDS=1` prints every posted event, such as `[uds] context 1 posted _UnlockTrophy {"_trophy_id":12}`, every statistic an event sets, such as `[uds] stat 10 of user 268435456 is 10 (COLLECT $.counter)`, and a `[uds]` line for a missing `trophyNN.ucp` or `udsNN.ucp`. An event that is not used prints the reason in a `[trophy] ignored` line: an `_UnlockTrophy` event without an int32 `_trophy_id` of 0 or more, or posted to a context that does not exist; and, once per rule, an event whose property is missing, negative, not an integer or outside the statistic's range. From f34bfc4f28ab04519dce4d47e7767d4ebc79b112 Mon Sep 17 00:00:00 2001 From: dantdmdarealguy Date: Wed, 21 Oct 2026 21:18:02 -0500 Subject: [PATCH 404/428] docs(debt): remove the sceVideoRecordingInit and sceVideoRecordingTerm entry now that they return 0 as the firmware does db2575839 (#8307) made both return 0, following their bodies in the firmware module libSceVideoRecording.sprx (xor eax, eax; ret), but left the entry saying they are parameterless stubs that throw. The firmware body leaves no open gap, so the entry goes. sceVideoRecordingGetStatus no longer lists sceVideoRecordingInit among the initializers that throw. --- docs/dev/debt/libSceVideoRecordingP.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceVideoRecordingP.md b/docs/dev/debt/libSceVideoRecordingP.md index 0b82a50648..835fa5eb21 100644 --- a/docs/dev/debt/libSceVideoRecordingP.md +++ b/docs/dev/debt/libSceVideoRecordingP.md @@ -8,7 +8,6 @@ - [sceVideoRecordingCloseByHandle, sceVideoRecordingGetInfoByHandle](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - native handles are context pointers. Recording state and per-handle queries remain unimplemented. - [sceVideoRecordingCopyBGRAtoNV12](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - the destination, source, 32-bit width and height arguments are confirmed. Pixel conversion remains unimplemented. - [sceVideoRecordingGetSupportInfo, sceVideoRecordingGetVideoOutMode](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - GetSupportInfo writes an 18-byte payload whose fields remain unresolved. GetVideoOutMode takes a four-byte output confirmed by the native recording assembly and Avcap2 IPC descriptor; the mode values remain unresolved. Both exports remain unimplemented. -- [sceVideoRecordingInit, sceVideoRecordingTerm](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - parameterless initialization and termination stubs; unimplemented and throw `NotImplemented_nid_no_patch` - [sceVideoRecordingInitialize](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp): the native initializer consumes no caller arguments; the former optional parameter is not read by the Avcap2 initializer or recording setup. Its unused parameter slot is retained because the protected body references it. - [sceVideoRecordingOpen, sceVideoRecordingOpen2](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - the 32-byte and 40-byte parameter variants, four argument registers and 32-bit heap size are recovered. Mode, output mode, option and format semantics remain incomplete. QueryHddSize2 returns a signed 64-bit size or error; recording and sizing exports remain unimplemented. @@ -16,4 +15,4 @@ ### Functional -- [sceVideoRecordingGetStatus](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) always returns 0, `None` ("The recorder is not open") in [SharpProspero's `VideoRecordingStatus`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/VideoRecording.cs#L18-L29), also listed in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/video_recording/video_recording.h#L32-L36). It is true here because `sceVideoRecordingOpen` throws, so no recorder is ever open. Unverified: whether the console returns 0 or an error before `sceVideoRecordingInit`/`sceVideoRecordingInitialize`, which throw, and when it reports `Running` (1) or `Paused` (2) +- [sceVideoRecordingGetStatus](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) always returns 0, `None` ("The recorder is not open") in [SharpProspero's `VideoRecordingStatus`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/VideoRecording.cs#L18-L29), also listed in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/video_recording/video_recording.h#L32-L36). It is true here because `sceVideoRecordingOpen` throws, so no recorder is ever open. Unverified: whether the console returns 0 or an error before `sceVideoRecordingInit`/`sceVideoRecordingInitialize`, and when it reports `Running` (1) or `Paused` (2) From f5757aab98617af9ee607ad6543828f36b66f493 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 25 Oct 2026 09:49:35 -0700 Subject: [PATCH 405/428] fix(libkernel): restore a handler's signal bit when the handler replaces the mask A handler that read the mask and put it back with SIG_SETMASK stored its own signal, which sigprocmask reports from the hold, in the process mask and cleared the hold, so the signal stayed blocked after the handler returned. pselect, setcontext and a siglongjmp inside the handler replace the mask the same way. When the handler's own thread releases a hold, the signal now gets back its bit from before the handler when the handler returns, as FreeBSD's sigreturn puts back the mask from before it. A mask another thread sets meanwhile is kept. --- .../libs/prx/libkernel/System/src/Signals.cpp | 37 +++++++++++--- core/libs/tests/GuestSignalHandlerMask.cpp | 50 +++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 81 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index a94bb62ff0..9ea06b2e2a 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -46,6 +46,7 @@ std::atomic blockedMask{0}; std::atomic blockedMaskExtra[3]{}; std::atomic handlerHolds[MaxSignal + 1]{}; constexpr std::uint64_t HoldCount = 0xffffffffu; +thread_local std::uint32_t holdReleases[MaxSignal + 1]{}; static_assert(std::atomic::is_always_lock_free); std::mutex registration; GuestSigaction dispositions[MaxSignal + 1]{}; @@ -180,12 +181,28 @@ void RunHandler(int guest, GuestHandler handler) { if ((flags & SaNodefer) == 0) mask[SignalWord(guest)] |= SignalBit(guest); mask[0] &= ~UnblockableSignals; if ((flags & SaResethand) != 0) ResetToDefault(guest); + std::uint32_t before[4]; + for (int word = 0; word < 4; ++word) before[word] = (word == 0 ? blockedMask.load() : blockedMaskExtra[word - 1].load()) & mask[word]; std::uint32_t epochs[MaxSignal + 1]{}; - for (int held = 1; held <= MaxSignal; ++held) - if (mask[SignalWord(held)] & SignalBit(held)) epochs[held] = static_cast(handlerHolds[held].fetch_add(1) >> 32); + std::uint32_t releases[MaxSignal + 1]{}; + for (int held = 1; held <= MaxSignal; ++held) { + if ((mask[SignalWord(held)] & SignalBit(held)) == 0) continue; + releases[held] = holdReleases[held]; + epochs[held] = static_cast(handlerHolds[held].fetch_add(1) >> 32); + } handler(guest); - for (int held = 1; held <= MaxSignal; ++held) - if (mask[SignalWord(held)] & SignalBit(held)) ReleaseHold(held, epochs[held]); + std::uint32_t replaced[4]{}; + for (int held = 1; held <= MaxSignal; ++held) { + if ((mask[SignalWord(held)] & SignalBit(held)) == 0) continue; + if (holdReleases[held] != releases[held]) replaced[SignalWord(held)] |= SignalBit(held); + ReleaseHold(held, epochs[held]); + } + for (int word = 0; word < 4; ++word) { + if (replaced[word] == 0) continue; + auto& target = word == 0 ? blockedMask : blockedMaskExtra[word - 1]; + target.fetch_and(~(replaced[word] & ~before[word])); + target.fetch_or(replaced[word] & before[word]); + } } void Dispatch(int native) { int guest = 0; @@ -539,14 +556,20 @@ int ChangeBlockedMask(int how, const GuestSignalSet* set, GuestSignalSet* previo blockedMask.fetch_and(~set->bits[0]); for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_and(~set->bits[word]); if (releaseHolds) - for (int guest = 1; guest <= MaxSignal; ++guest) - if (SignalInSet(*set, guest)) ClearHold(guest); + for (int guest = 1; guest <= MaxSignal; ++guest) { + if (!SignalInSet(*set, guest)) continue; + ClearHold(guest); + ++holdReleases[guest]; + } break; case 3: blockedMask.store(set->bits[0] & ~UnblockableSignals); for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].store(set->bits[word]); if (releaseHolds) - for (int guest = 1; guest <= MaxSignal; ++guest) ClearHold(guest); + for (int guest = 1; guest <= MaxSignal; ++guest) { + ClearHold(guest); + ++holdReleases[guest]; + } break; } } diff --git a/core/libs/tests/GuestSignalHandlerMask.cpp b/core/libs/tests/GuestSignalHandlerMask.cpp index bd5864542d..36029bfd31 100644 --- a/core/libs/tests/GuestSignalHandlerMask.cpp +++ b/core/libs/tests/GuestSignalHandlerMask.cpp @@ -14,6 +14,7 @@ static constexpr int SaResethand = 0x4; static constexpr int SaNodefer = 0x10; static constexpr int SigBlock = 1; static constexpr int SigUnblock = 2; +static constexpr int SigSetmask = 3; static std::uint32_t Bit(int signal) { return 1u << (signal - 1); } @@ -56,6 +57,29 @@ static void APS5_VABI BlockFromOtherThread(int) { }).join(); } +static void APS5_VABI SetMaskFromOtherThread(int) { + ++calls; + std::thread([] { + const GuestSignalSet set{{Bit(SigTerm), 0, 0, 0}}; + Require(sigprocmask_nid_postfix(SigSetmask, &set, nullptr) == 0); + }).join(); +} + +static void APS5_VABI SaveAndRestoreMask(int) { + ++calls; + GuestSignalSet old{}; + Require(sigprocmask_nid_postfix(SigBlock, nullptr, &old) == 0); + Require(sigprocmask_nid_postfix(SigSetmask, &old, nullptr) == 0); +} + +static void APS5_VABI ReplaceAndRestoreMask(int) { + ++calls; + const GuestSignalSet none{}; + GuestSignalSet old{}; + Require(sigprocmask_nid_postfix(SigSetmask, &none, &old) == 0); + Require(sigprocmask_nid_postfix(SigSetmask, &old, nullptr) == 0); +} + static GuestSignalSet setInHandler{}; static void APS5_VABI RecordMask(int) { @@ -131,6 +155,28 @@ static void CheckOtherThreadKeptSameSignal() { Require(CurrentMask() == 0); } +static void CheckOtherThreadSetMaskKept() { + calls = 0; + Install(SigUsr1, SetMaskFromOtherThread, SaRestart, 0); + Require(raise_nid_postfix(SigUsr1) == 0); + Require(calls == 1); + Require(CurrentMask() == Bit(SigTerm)); + const GuestSignalSet none{}; + Require(sigprocmask_nid_postfix(SigSetmask, &none, nullptr) == 0); + Require(CurrentMask() == 0); +} + +static void CheckMaskRestoredInsideHandler(int signal, void (APS5_VABI* handler)(int)) { + calls = 0; + Install(signal, handler, SaRestart, 0); + Require(raise_nid_postfix(signal) == 0); + Require(calls == 1); + Require(CurrentMask() == 0); + Require(raise_nid_postfix(signal) == 0); + Require(calls == 2); + Require(CurrentMask() == 0); +} + static void CheckUnblockInsideHandler() { calls = 0; callsAfterNestedRaise = 0; @@ -172,6 +218,10 @@ int main() { CheckResethand(); CheckOtherThreadKept(); CheckOtherThreadKeptSameSignal(); + CheckOtherThreadSetMaskKept(); + CheckMaskRestoredInsideHandler(SigUsr2, SaveAndRestoreMask); + CheckMaskRestoredInsideHandler(SigTerm, SaveAndRestoreMask); + CheckMaskRestoredInsideHandler(SigUsr2, ReplaceAndRestoreMask); CheckUnblockInsideHandler(); CheckUpperMaskWords(); CheckLeftWithLongjmp(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 22b71cf8fe..3b1ad4d1d4 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -251,7 +251,7 @@ - [sigtimedwait](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) waits on a process-wide pending set instead of throwing: a signal raised while blocked stays pending and is taken by `sigwait`, `sigwaitinfo` or `sigtimedwait`, delivered to the handler when `_sigprocmask` unblocks it, or delivered by `pause`. The set covers all 128 signals and the lowest number that is pending and in the set is selected (FreeBSD specifies the lowest number only among real-time signals). Selection, `EAGAIN` on timeout, `EINVAL` for a `tv_nsec` outside 0 to 1000000000 while nothing is pending and `NULL` timeout blocking indefinitely follow FreeBSD's [sigtimedwait(2)](https://man.freebsd.org/cgi/man.cgi?query=sigtimedwait). `sigwait` returns 0 with the number in its output, as POSIX specifies, and `sigwaitinfo` and `sigtimedwait` return the number, as FreeBSD does. The guest `siginfo_t` follows FreeBSD 11's `sys/sys/siginfo.h`: 80 bytes, `si_signo` at 0, `si_code` at 8, `si_pid` at 12, `si_uid` at 16, `si_addr` at 24, `si_value` at 32 and `si_band` at 40; only `si_signo`, `si_code` (`SI_USER`) and `si_pid` (the guest pid) are written, `si_uid` stays 0 because no uid is modelled, and the console's layout is not verified. PPSA85300 imports `sigtimedwait` and fails to load without it; its only call site is VLC's `vlc_writev` ([filesystem.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/posix/filesystem.c#L243-L278)), which polls with a zero timeout to drain a `SIGPIPE` only after `writev` fails with `EPIPE`. The waits are not cancellation points and an unblocked caught signal does not interrupt them with `EINTR`, where FreeBSD's `sigwaitinfo` and `sigtimedwait` may return it. `SIGPIPE` has no host equivalent and is never raised by a host write here (see `read, write and vector I/O`), so VLC's drain finds nothing - [pause, sigsuspend, sigwait, sigwaitinfo](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) take the pending set of `sigtimedwait` instead of throwing. `pause` waits for a pending signal that is not blocked and whose disposition can be delivered, runs its handler on the calling thread and returns -1 with `EINTR`; an ignored signal is discarded and the wait continues, and it never returns for a signal whose handler cannot run. A signal whose registered handler has `SA_SIGINFO` still throws, and one whose disposition is the default action and that has no host equivalent (every signal except 2, 4, 6, 8, 11 and 15) is neither delivered nor discarded: it stays pending until a wait takes it, where FreeBSD runs the default action, which terminates the process for most of them. A pending `SIGALRM` with the default disposition exits the process with status 142 once it is unblocked, as FreeBSD's default action does. PPSA68091, PPSA99169, PPSA99420 and PPSA97358 import them and fail to load without them. PPSA68091 calls `pause` at one site (0x8F7266), in a loop that waits while a global holds another thread's `errno` address. PPSA99169 only stores the address of `pause` in a table of symbol addresses (0x30DB1). PPSA99420 calls `pause` at 0x3BD80F6 and 0x3C9E220, `sigwait` at 0x3C9E4C7 and `sigwaitinfo` at 0x3C9E588. PPSA97358 calls `sigwait` from QEMU's `sigwait_compat` thread ([compatfd.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/compatfd.c#L28-L53)), which `qemu_signal_init` ([main-loop.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/main-loop.c#L110-L147)) starts for `SIGIO`, `SIGALRM` and `SIGBUS` when the main loop is created, so it reaches the wait at start-up, and from the vCPU thread of the dummy accelerator ([dummy-cpus.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/accel/dummy-cpus.c#L48-L56)). `sigsuspend` swaps the process-wide mask of `_sigprocmask` for its argument, waits as `pause` does through the same helper, then restores the mask through `_sigprocmask`, and returns -1 with `EINTR`; a null set throws, as the console faults in libthr instead of returning `EFAULT`. All four words of the argument replace the mask and are restored afterwards, as in `_sigprocmask`. Java 25 LAN / DNS (PPSA99869, #5501) imports it and fails to load without it, after `sysctlnametomib`, `fegetenv` and `mincore`. None of the five waits is a cancellation point - [getsid, setpgid, setpgrp](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - getsid returns the calling process ID for pid 0 and the calling process, and `ESRCH` for other process IDs, assuming the single guest process is its own session leader. setpgid and setpgrp follow FreeBSD 11.4's `sys_setpgid` for the only guest process: a negative group ID fails with `EINVAL`, a process ID other than 0 or the caller's PID (including a negative PID) fails with `ESRCH`, and because the single guest process is modeled as its own session leader, `SESS_LEADER(targp)` fails with `EPERM` before normalizing the process group or reaching the same-process-group no-op. PPSA74126 imports `getsid` and fails to load without it -- [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do. While a handler runs, its own signal (unless `SA_NODEFER`) and the signals of its mask other than `SIGKILL` and `SIGSTOP` are blocked, and `SA_RESETHAND` resets the disposition to the default before it runs, as FreeBSD's `postsig_done` does. Those blocks are held apart from the process-wide mask that `sigprocmask` sets, which reports them, and are released when the handler returns, so a block another thread adds meanwhile is kept, for the same signal too, and so is one the handler adds itself, which FreeBSD's `sigreturn` would undo. `SIG_UNBLOCK` of a held signal releases its hold and `SIG_SETMASK` releases them all; a `sigsuspend` inside a handler keeps them, where FreeBSD replaces the whole thread mask. A handler left with `longjmp`, or with `siglongjmp` to a buffer that saved the mask, gets back the mask that buffer saved, as on FreeBSD; a signal raised meanwhile stays pending and is delivered when the handler returns to `raise`, but one left pending by a handler that a host signal ran asynchronously waits for the next `raise` or `_sigprocmask`. `SA_ONSTACK` is not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating +- [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do. While a handler runs, its own signal (unless `SA_NODEFER`) and the signals of its mask other than `SIGKILL` and `SIGSTOP` are blocked, and `SA_RESETHAND` resets the disposition to the default before it runs, as FreeBSD's `postsig_done` does. Those blocks are held apart from the process-wide mask that `sigprocmask` sets, which reports them, and are released when the handler returns, so a block another thread adds meanwhile is kept, for the same signal too, and so is one the handler adds itself, which FreeBSD's `sigreturn` would undo. `SIG_UNBLOCK` of a held signal releases its hold and `SIG_SETMASK` releases them all. When the handler's own thread released one, that signal gets back its bit of the process mask from before the handler when the handler returns, as FreeBSD's `sigreturn` puts back the mask from before it, so a handler that saves and restores the mask with `SIG_SETMASK`, or calls `pselect`, leaves its signal unblocked. A `sigsuspend` inside a handler keeps the holds, where FreeBSD replaces the whole thread mask. A handler left with `longjmp`, or with `siglongjmp` to a buffer that saved the mask, gets back the mask that buffer saved, as on FreeBSD; a signal raised meanwhile stays pending and is delivered when the handler returns to `raise`, but one left pending by a handler that a host signal ran asynchronously waits for the next `raise` or `_sigprocmask`. `SA_ONSTACK` is not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating - [signal](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records its handler with `SA_RESTART` unless the signal is in the exported `_sigintr` set, as FreeBSD's libc does (`lib/libc/gen/signal.c`). `siginterrupt`, which fills that set, is a libSceLibcInternal export that libc serves through the `sigaction` and the set libkernel registers at load (see libc). - [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console From 0b60e97834ad06a57519f5154eedff0947fdeeda Mon Sep 17 00:00:00 2001 From: zaryabzip <110787259+zaryabzip@users.noreply.github.com> Date: Thu, 22 Oct 2026 05:57:17 +0500 Subject: [PATCH 406/428] feat(libkernel_sys): create threads suspended and resume them Implement pthread_attr_setcreatesuspend_np, pthread_resume_np, pthread_resume_all_np and pthread_multi_np with the sce forms scePthreadAttrSetcreatesuspend, scePthreadResume, scePthreadResumeAll and scePthreadMulti, following FreeBSD 11.4 libthr and the firmware 13.42 libkernel_sys module. A thread created suspended waits before its start routine, outside any cancellation point, until it is resumed. sceKernelRaiseException throws on Windows for a thread still suspended at creation. --- core/libs/CMakeLists.txt | 11 + .../libs/prx/libkernel/Pthread/Posix/Attr.cpp | 1 + .../prx/libkernel/Pthread/include/Pthread.hpp | 5 + core/libs/prx/libkernel/Pthread/src/Attr.cpp | 1 + .../libs/prx/libkernel/Pthread/src/Thread.cpp | 23 ++ .../prx/libkernel/System/src/Exception.cpp | 1 + .../prx/libkernel_sys/Export.Discovery.cpp | 44 +-- core/libs/prx/libkernel_sys/include/Api.hpp | 12 +- .../tests/GuestPthreadCreateSuspended.cpp | 276 ++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + docs/dev/debt/libkernel_sys.md | 1 + 11 files changed, 350 insertions(+), 26 deletions(-) create mode 100644 core/libs/tests/GuestPthreadCreateSuspended.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index eea852c88a..ee3dfbd9f2 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -511,6 +511,17 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_create_suspended_tests tests/GuestPthreadCreateSuspended.cpp) + target_include_directories(guest_pthread_create_suspended_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_pthread_create_suspended_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_pthread_create_suspended_tests) + add_test(NAME guest_pthread_create_suspended COMMAND guest_pthread_create_suspended_tests) + set_tests_properties(guest_pthread_create_suspended PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_create_suspended PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_np_party_tests tests/GuestNpParty.cpp) target_include_directories(guest_np_party_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_np_party_tests PRIVATE libSceNpParty libc) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp index a71ae71597..35a9259df9 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp @@ -30,6 +30,7 @@ int APS5_VABI pthread_attr_get_np_nid_postfix(Pthread thread, PthreadAttr* attr) p->_detachstate = thread->_detached ? 1 : 0; p->_schedpriority = thread->priority.load(std::memory_order_relaxed); p->_schedpolicy = thread->schedPolicy.load(std::memory_order_relaxed); + p->_createSuspended = thread->createdSuspended; p->_affinity = thread->affinity.load(std::memory_order_relaxed); p->_affinityExplicit = true; p->_scope = thread->scope; diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index 0e83376197..097d609562 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -110,6 +110,7 @@ struct PthreadAttrPrivate { int _schedpriority; int _schedpolicy; int _inheritsched; + bool _createSuspended = false; KernelCpumask _affinity = DEFAULT_THREAD_AFFINITY; bool _affinityExplicit = false; std::size_t _guardsize = 0x1000; @@ -130,6 +131,8 @@ struct PthreadPrivate { std::atomic references{2}; std::atomic inWait{false}; std::atomic pendingException{0}; + std::atomic suspended{false}; + bool createdSuspended = false; std::atomic schedPolicy{1}; void* wakeEvent = nullptr; void* stackAddress = nullptr; @@ -179,6 +182,8 @@ void SetLiveGuestThreadsAffinity(KernelCpumask mask); KernelCpumask LiveGuestThreadsAffinity(); bool GuestThreadIsLive(const PthreadPrivate* thread); std::unique_lock LockLiveGuestThread(const PthreadPrivate* thread); +extern "C" int ResumeGuestThread_nid_no_patch(PthreadPrivate* thread); +extern "C" void ResumeAllGuestThreads_nid_no_patch(); bool LiveGuestThreadExists(std::int64_t tid); bool GetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask& mask); bool GetLiveGuestThreadSched_nid_no_patch(std::int64_t tid, int& policy, int& priority); diff --git a/core/libs/prx/libkernel/Pthread/src/Attr.cpp b/core/libs/prx/libkernel/Pthread/src/Attr.cpp index 0c26eb6762..9b7cb2e521 100644 --- a/core/libs/prx/libkernel/Pthread/src/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Attr.cpp @@ -98,6 +98,7 @@ int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr) { (*attr)->_schedpriority = thread->priority.load(std::memory_order_relaxed); (*attr)->_schedpolicy = thread->schedPolicy.load(std::memory_order_relaxed); (*attr)->_inheritsched = 4; + (*attr)->_createSuspended = thread->createdSuspended; (*attr)->_affinity = thread->affinity.load(std::memory_order_relaxed); (*attr)->_affinityExplicit = true; (*attr)->_scope = thread->scope; diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index d4505b8d1b..71680fca25 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -555,6 +555,26 @@ bool GuestThreadIsLive(const PthreadPrivate* thread) { return LockLiveGuestThread(thread).owns_lock(); } +extern "C" int ResumeGuestThread_nid_no_patch(PthreadPrivate* thread) { + constexpr int GuestEsrch = 3; + const auto live = LockLiveGuestThread(thread); + if (!live.owns_lock()) return GuestEsrch; + thread->suspended.store(false, std::memory_order_release); + thread->suspended.notify_all(); + return 0; +} + +extern "C" void ResumeAllGuestThreads_nid_no_patch() { + const auto* self = CurrentGuestState().current; + auto& registry = CpuClocks(); + std::lock_guard lock(registry.lock); + for (const auto& [id, thread] : registry.threads) { + if (thread == self) continue; + thread->suspended.store(false, std::memory_order_release); + thread->suspended.notify_all(); + } +} + bool GetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask& mask) { auto& registry = CpuClocks(); std::lock_guard lock(registry.lock); @@ -775,6 +795,7 @@ static void RunThread(std::unique_ptr args) { const sigset_t signalMask = args->signalMask; #endif args.reset(); + self->suspended.wait(true, std::memory_order_acquire); #ifndef _WIN32 pthread_sigmask(SIG_SETMASK, &signalMask, nullptr); #endif @@ -950,6 +971,8 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread p->scope = CurrentGuestState().current->scope; } if (name) p->name = name; + p->createdSuspended = attr && (*attr)->_createSuspended; + p->suspended.store(p->createdSuspended, std::memory_order_relaxed); std::promise start; auto args = std::make_unique(ThreadArgs{entry, arg, p.get()}); #ifdef _WIN32 diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index 8882083584..73b40090f1 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -681,6 +681,7 @@ int APS5_VABI sceKernelRaiseException(Pthread thread, int signum) { const auto handler = Handler(signum); if (handler == nullptr) throw std::runtime_error("sceKernelRaiseException: no handler installed for the signal"); #ifdef _WIN32 + if (thread->suspended.load(std::memory_order_acquire)) throw std::runtime_error("sceKernelRaiseException: the target thread is still suspended at creation"); return RaiseOn(thread, handler, signum) ? 0 : SCE_KERNEL_ERROR_ESRCH; #else return RaiseOn(thread) ? 0 : SCE_KERNEL_ERROR_ESRCH; diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 3addcda651..e728785242 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -1,3 +1,5 @@ +#include "prx/libkernel/KernelErrors.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" #include "prx/libkernel_sys/include/Api.hpp" #include "prx/libkernel/Pthread/include/Atfork.hpp" #include "prx/libkernel/include/Api.hpp" @@ -287,8 +289,10 @@ int APS5_VABI pthread_attr_setaffinity_np_nid_postfix(void) { return 0; } -int APS5_VABI pthread_attr_setcreatesuspend_np_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI pthread_attr_setcreatesuspend_np_nid_postfix(PthreadAttr* attr) { + constexpr int GuestEinval = 22; + if (!attr || !*attr) return GuestEinval; + (*attr)->_createSuspended = true; return 0; } @@ -323,7 +327,7 @@ int APS5_VABI pthread_getstack_np_nid_postfix(void) { } int APS5_VABI pthread_multi_np_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); + pthread_resume_all_np_nid_postfix(); return 0; } @@ -357,14 +361,15 @@ int APS5_VABI pthread_mutexattr_setgen_np_nid_postfix(void) { return 0; } -int APS5_VABI pthread_resume_all_np_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI pthread_resume_all_np_nid_postfix(void) { + ResumeAllGuestThreads_nid_no_patch(); + scePthreadTestcancel(); } -int APS5_VABI pthread_resume_np_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI pthread_resume_np_nid_postfix(Pthread thread) { + constexpr int GuestEinval = 22; + if (!thread) return GuestEinval; + return ResumeGuestThread_nid_no_patch(thread); } int APS5_VABI pthread_resume_user_context_np_nid_postfix(void) { @@ -2144,9 +2149,9 @@ int APS5_VABI scePthreadAttrGetscope(const PthreadAttr* attr, int* scope) { return SceResult(pthread_attr_getscope_nid_postfix(attr, scope)); } -int APS5_VABI scePthreadAttrSetcreatesuspend(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadAttrSetcreatesuspend(PthreadAttr* attr) { + const int error = pthread_attr_setcreatesuspend_np_nid_postfix(attr); + return error == 0 ? 0 : SceKernelError(error); } int APS5_VABI scePthreadAttrSetscope(PthreadAttr* attr, int scope) { @@ -2197,8 +2202,8 @@ int APS5_VABI scePthreadMain(void) { } int APS5_VABI scePthreadMulti(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + const int error = pthread_multi_np_nid_postfix(); + return error == 0 ? 0 : SceKernelError(error); } int APS5_VABI scePthreadMutexGetprioceiling(void) { @@ -2246,14 +2251,13 @@ int APS5_VABI scePthreadMutexattrSetkind(void) { return 0; } -int APS5_VABI scePthreadResume(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePthreadResume(Pthread thread) { + const int error = pthread_resume_np_nid_postfix(thread); + return error == 0 ? 0 : SceKernelError(error); } -int APS5_VABI scePthreadResumeAll(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI scePthreadResumeAll(void) { + pthread_resume_all_np_nid_postfix(); } int APS5_VABI scePthreadSetBesteffort(void) { diff --git a/core/libs/prx/libkernel_sys/include/Api.hpp b/core/libs/prx/libkernel_sys/include/Api.hpp index 938bc8178b..9e555cbae1 100644 --- a/core/libs/prx/libkernel_sys/include/Api.hpp +++ b/core/libs/prx/libkernel_sys/include/Api.hpp @@ -124,7 +124,7 @@ int APS5_VABI profil_nid_postfix(void); int APS5_VABI pthread_attr_getaffinity_np_nid_postfix(void); int APS5_VABI pthread_attr_getsolosched_np_nid_postfix(void); int APS5_VABI pthread_attr_setaffinity_np_nid_postfix(void); -int APS5_VABI pthread_attr_setcreatesuspend_np_nid_postfix(void); +int APS5_VABI pthread_attr_setcreatesuspend_np_nid_postfix(PthreadAttr* attr); int APS5_VABI pthread_attr_setstackaddr_nid_postfix(void); int APS5_VABI pthread_barrier_setname_np_nid_postfix(void); int APS5_VABI pthread_cond_setname_np_nid_postfix(void); @@ -139,8 +139,8 @@ int APS5_VABI pthread_mutex_setname_np_nid_postfix(void); int APS5_VABI pthread_mutex_setprioceiling_nid_postfix(void); int APS5_VABI pthread_mutexattr_getgen_np_nid_postfix(void); int APS5_VABI pthread_mutexattr_setgen_np_nid_postfix(void); -int APS5_VABI pthread_resume_all_np_nid_postfix(void); -int APS5_VABI pthread_resume_np_nid_postfix(void); +void APS5_VABI pthread_resume_all_np_nid_postfix(void); +int APS5_VABI pthread_resume_np_nid_postfix(Pthread thread); int APS5_VABI pthread_resume_user_context_np_nid_postfix(void); int APS5_VABI pthread_rwlock_setname_np_nid_postfix(void); int APS5_VABI pthread_rwlockattr_settype_np_nid_postfix(void); @@ -497,7 +497,7 @@ int APS5_VABI sceKernelYieldCpumode(void); int APS5_VABI sceLibcMspaceCreateForMonoMutex(void); int APS5_VABI scePthreadAtfork(void (APS5_VABI* prepare)(), void (APS5_VABI* parent)(), void (APS5_VABI* child)()); int APS5_VABI scePthreadAttrGetscope(const PthreadAttr* attr, int* scope); -int APS5_VABI scePthreadAttrSetcreatesuspend(void); +int APS5_VABI scePthreadAttrSetcreatesuspend(PthreadAttr* attr); int APS5_VABI scePthreadAttrSetscope(PthreadAttr* attr, int scope); int APS5_VABI scePthreadBarrierDestroy(PthreadBarrierPrivate** barrier); int APS5_VABI scePthreadBarrierInit(PthreadBarrierPrivate** barrier, PthreadBarrierattrPrivate* const* attr, unsigned count, const char* name); @@ -519,8 +519,8 @@ int APS5_VABI scePthreadMutexattrGetkind(void); int APS5_VABI scePthreadMutexattrInitForInternalLibc(void); int APS5_VABI scePthreadMutexattrSetgen(void); int APS5_VABI scePthreadMutexattrSetkind(void); -int APS5_VABI scePthreadResume(void); -int APS5_VABI scePthreadResumeAll(void); +int APS5_VABI scePthreadResume(Pthread thread); +void APS5_VABI scePthreadResumeAll(void); int APS5_VABI scePthreadSetBesteffort(void); int APS5_VABI scePthreadSetconcurrency(int level); int APS5_VABI scePthreadSingle(void); diff --git a/core/libs/tests/GuestPthreadCreateSuspended.cpp b/core/libs/tests/GuestPthreadCreateSuspended.cpp new file mode 100644 index 0000000000..cadde5c8e2 --- /dev/null +++ b/core/libs/tests/GuestPthreadCreateSuspended.cpp @@ -0,0 +1,276 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel_sys/include/Api.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016u); +static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast(0x80020003u); +static constexpr int GUEST_ESRCH = 3; +static constexpr int GUEST_EINVAL = 22; +static constexpr int GUEST_SIGUSR1 = 30; +static constexpr int GUEST_CREATE_DETACHED = 1; +static constexpr int DETACHED_ROUNDS = 200; +static constexpr auto SUSPENDED_WINDOW = std::chrono::milliseconds(50); +static void* const CANCELED = reinterpret_cast(std::uintptr_t{1}); +static void* const FINISHED = reinterpret_cast(std::uintptr_t{7}); + +static void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +struct Started { + std::atomic ran{false}; + std::atomic returned{false}; +}; + +static std::atomic release{false}; +static std::atomic stopResuming{false}; +static std::atomic raised{0}; +static std::atomic raisedOn{nullptr}; + +static void* APS5_VABI Record(void* opaque) { + static_cast(opaque)->ran.store(true); + return FINISHED; +} + +static void* APS5_VABI CancelAfterResume(void* opaque) { + auto* started = static_cast(opaque); + started->ran.store(true); + scePthreadTestcancel(); + started->returned.store(true); + return FINISHED; +} + +static void* APS5_VABI CancelInResumeAll(void* opaque) { + auto* started = static_cast(opaque); + started->ran.store(true); + Require(scePthreadCancel(scePthreadSelf()) == SCE_OK, "scePthreadCancel of the calling thread failed"); + pthread_resume_all_np_nid_postfix(); + started->returned.store(true); + return FINISHED; +} + +static void* APS5_VABI ResumeAllUntilStopped(void*) { + while (!stopResuming.load()) pthread_resume_all_np_nid_postfix(); + return FINISHED; +} + +static void* APS5_VABI WaitForRelease(void*) { + while (!release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + return FINISHED; +} + +static void APS5_VABI Handler(int, void*) { + raisedOn.store(scePthreadSelf()); + raised.fetch_add(1); +} + +static void CreateSuspendedPosix(Pthread* thread, PthreadEntry entry, void* arg) { + PthreadAttr attr = nullptr; + Require(pthread_attr_init_nid_postfix(&attr) == 0, "pthread_attr_init failed"); + Require(pthread_attr_setcreatesuspend_np_nid_postfix(&attr) == 0, "pthread_attr_setcreatesuspend_np failed"); + Require(pthread_create_nid_postfix(thread, &attr, entry, arg) == 0, "pthread_create of a suspended thread failed"); + Require(pthread_attr_destroy_nid_postfix(&attr) == 0, "pthread_attr_destroy failed"); +} + +static void CreateSuspendedSce(Pthread* thread, PthreadEntry entry, void* arg) { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK, "scePthreadAttrInit failed"); + Require(scePthreadAttrSetcreatesuspend(&attr) == SCE_OK, "scePthreadAttrSetcreatesuspend failed"); + Require(scePthreadCreate(thread, &attr, entry, arg, "suspended") == SCE_OK, "scePthreadCreate of a suspended thread failed"); + Require(scePthreadAttrDestroy(&attr) == SCE_OK, "scePthreadAttrDestroy failed"); +} + +static void ReportsErrors() { + Require(pthread_attr_setcreatesuspend_np_nid_postfix(nullptr) == GUEST_EINVAL, "pthread_attr_setcreatesuspend_np accepted a null attribute"); + Require(scePthreadAttrSetcreatesuspend(nullptr) == SCE_KERNEL_ERROR_EINVAL, "scePthreadAttrSetcreatesuspend accepted a null attribute"); + PthreadAttr destroyed = nullptr; + Require(scePthreadAttrInit(&destroyed) == SCE_OK, "scePthreadAttrInit failed"); + Require(scePthreadAttrDestroy(&destroyed) == SCE_OK, "scePthreadAttrDestroy failed"); + Require(pthread_attr_setcreatesuspend_np_nid_postfix(&destroyed) == GUEST_EINVAL, "pthread_attr_setcreatesuspend_np accepted a destroyed attribute"); + Require(scePthreadAttrSetcreatesuspend(&destroyed) == SCE_KERNEL_ERROR_EINVAL, "scePthreadAttrSetcreatesuspend accepted a destroyed attribute"); + Require(pthread_resume_np_nid_postfix(nullptr) == GUEST_EINVAL, "pthread_resume_np accepted a null thread"); + Require(scePthreadResume(nullptr) == SCE_KERNEL_ERROR_EINVAL, "scePthreadResume accepted a null thread"); + Pthread joined = nullptr; + Started started; + Require(scePthreadCreate(&joined, nullptr, Record, &started, "joined") == SCE_OK, "scePthreadCreate failed"); + Require(scePthreadJoin(joined, nullptr) == SCE_OK, "scePthreadJoin failed"); + Require(pthread_resume_np_nid_postfix(joined) == GUEST_ESRCH, "pthread_resume_np found a joined thread"); + Require(scePthreadResume(joined) == SCE_KERNEL_ERROR_ESRCH, "scePthreadResume found a joined thread"); +} + +static void ResumesOneThread(bool sce) { + Started started; + Pthread thread = nullptr; + if (sce) CreateSuspendedSce(&thread, Record, &started); + else CreateSuspendedPosix(&thread, Record, &started); + std::this_thread::sleep_for(SUSPENDED_WINDOW); + Require(!started.ran.load(), "a thread created suspended ran before it was resumed"); + Require((sce ? scePthreadResume(thread) : pthread_resume_np_nid_postfix(thread)) == 0, "resuming a suspended thread failed"); + void* result = nullptr; + Require((sce ? scePthreadJoin(thread, &result) : pthread_join_nid_postfix(thread, &result)) == 0, "joining a resumed thread failed"); + Require(result == FINISHED && started.ran.load(), "a resumed thread did not run its start routine"); +} + +static void ResumesRunningAndCallingThreads() { + Require(pthread_resume_np_nid_postfix(scePthreadSelf()) == 0, "pthread_resume_np of the calling thread failed"); + release.store(false); + Pthread running = nullptr; + Require(scePthreadCreate(&running, nullptr, WaitForRelease, nullptr, "running") == SCE_OK, "scePthreadCreate failed"); + Require(pthread_resume_np_nid_postfix(running) == 0, "pthread_resume_np of a running thread failed"); + release.store(true); + void* result = nullptr; + Require(scePthreadJoin(running, &result) == SCE_OK && result == FINISHED, "joining a running thread failed"); +} + +static void ResumesAllThreads(void (*resumeAll)()) { + Started first; + Started second; + Pthread firstThread = nullptr; + Pthread secondThread = nullptr; + CreateSuspendedPosix(&firstThread, Record, &first); + CreateSuspendedSce(&secondThread, Record, &second); + std::this_thread::sleep_for(SUSPENDED_WINDOW); + Require(!first.ran.load() && !second.ran.load(), "a thread created suspended ran before it was resumed"); + resumeAll(); + void* result = nullptr; + Require(scePthreadJoin(firstThread, &result) == SCE_OK && result == FINISHED && first.ran.load(), "resuming all threads left the first one suspended"); + Require(scePthreadJoin(secondThread, &result) == SCE_OK && result == FINISHED && second.ran.load(), "resuming all threads left the second one suspended"); +} + +static void CancelsBeforeResume() { + Started started; + Pthread thread = nullptr; + CreateSuspendedSce(&thread, CancelAfterResume, &started); + Require(scePthreadCancel(thread) == SCE_OK, "scePthreadCancel of a suspended thread failed"); + Require(pthread_resume_np_nid_postfix(thread) == 0, "pthread_resume_np of a canceled thread failed"); + void* result = nullptr; + Require(scePthreadJoin(thread, &result) == SCE_OK, "joining a canceled thread failed"); + Require(result == CANCELED && started.ran.load() && !started.returned.load(), "a cancel requested while suspended did not take effect at the first cancellation point"); +} + +static void CancelsInResumeAll() { + Started started; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, CancelInResumeAll, &started, "resume_all") == SCE_OK, "scePthreadCreate failed"); + void* result = nullptr; + Require(scePthreadJoin(thread, &result) == SCE_OK, "joining a canceled thread failed"); + Require(result == CANCELED && started.ran.load() && !started.returned.load(), "pthread_resume_all_np did not act on a pending cancel"); +} + +static void ReportsCreateSuspendedInAttrGet(int (APS5_VABI* get)(Pthread, PthreadAttr*)) { + Started first; + Started second; + Pthread suspended = nullptr; + CreateSuspendedPosix(&suspended, Record, &first); + PthreadAttr attr = nullptr; + Require(pthread_attr_init_nid_postfix(&attr) == 0, "pthread_attr_init failed"); + Require(get(suspended, &attr) == 0, "getting the attributes of a suspended thread failed"); + Pthread copy = nullptr; + Require(pthread_create_nid_postfix(©, &attr, Record, &second) == 0, "pthread_create with the attributes of a suspended thread failed"); + std::this_thread::sleep_for(SUSPENDED_WINDOW); + Require(!second.ran.load(), "a thread created with the attributes of a suspended thread ran before it was resumed"); + Require(pthread_resume_np_nid_postfix(copy) == 0 && pthread_resume_np_nid_postfix(suspended) == 0, "resuming a suspended thread failed"); + void* result = nullptr; + Require(pthread_join_nid_postfix(copy, &result) == 0 && result == FINISHED && second.ran.load(), "joining a resumed thread failed"); + Require(pthread_join_nid_postfix(suspended, &result) == 0 && result == FINISHED && first.ran.load(), "joining a resumed thread failed"); + release.store(false); + Pthread running = nullptr; + Require(scePthreadCreate(&running, nullptr, WaitForRelease, nullptr, "running") == SCE_OK, "scePthreadCreate failed"); + Require(pthread_attr_setcreatesuspend_np_nid_postfix(&attr) == 0, "pthread_attr_setcreatesuspend_np failed"); + Require(get(running, &attr) == 0, "getting the attributes of a running thread failed"); + Started third; + Pthread plain = nullptr; + Require(pthread_create_nid_postfix(&plain, &attr, Record, &third) == 0, "pthread_create with the attributes of a running thread failed"); + Require(pthread_join_nid_postfix(plain, &result) == 0 && result == FINISHED && third.ran.load(), "a thread created with the attributes of a running thread did not run"); + release.store(true); + Require(scePthreadJoin(running, nullptr) == SCE_OK, "joining a running thread failed"); + Require(pthread_attr_destroy_nid_postfix(&attr) == 0, "pthread_attr_destroy failed"); +} + +static void ResumesDetachedThreadsWhileTheyExit() { + stopResuming.store(false); + Pthread resumer = nullptr; + Require(scePthreadCreate(&resumer, nullptr, ResumeAllUntilStopped, nullptr, "resumer") == SCE_OK, "scePthreadCreate failed"); + for (int round = 0; round < DETACHED_ROUNDS; ++round) { + Started started; + PthreadAttr attr = nullptr; + Require(pthread_attr_init_nid_postfix(&attr) == 0, "pthread_attr_init failed"); + Require(pthread_attr_setcreatesuspend_np_nid_postfix(&attr) == 0, "pthread_attr_setcreatesuspend_np failed"); + Require(pthread_attr_setdetachstate_nid_postfix(&attr, GUEST_CREATE_DETACHED) == 0, "pthread_attr_setdetachstate failed"); + Pthread thread = nullptr; + Require(pthread_create_nid_postfix(&thread, &attr, Record, &started) == 0, "pthread_create of a detached suspended thread failed"); + Require(pthread_attr_destroy_nid_postfix(&attr) == 0, "pthread_attr_destroy failed"); + int result = 0; + while ((result = pthread_resume_np_nid_postfix(thread)) == 0) std::this_thread::yield(); + Require(result == GUEST_ESRCH && started.ran.load(), "a detached thread resumed while it exited did not run its start routine"); + } + stopResuming.store(true); + Require(scePthreadJoin(resumer, nullptr) == SCE_OK, "joining the resuming thread failed"); +} + +#ifdef _WIN32 +static void RefusesRaiseWhileSuspended() { + Require(sceKernelInstallExceptionHandler(GUEST_SIGUSR1, reinterpret_cast(&Handler)) == SCE_OK, "sceKernelInstallExceptionHandler failed"); + Started started; + Pthread thread = nullptr; + CreateSuspendedSce(&thread, Record, &started); + bool refused = false; + try { + sceKernelRaiseException(thread, GUEST_SIGUSR1); + } catch (const std::runtime_error& error) { + refused = std::string(error.what()).find("suspended") != std::string::npos; + } + Require(refused, "sceKernelRaiseException did not refuse a thread still suspended at creation"); + Require(pthread_resume_np_nid_postfix(thread) == 0, "pthread_resume_np failed"); + Require(scePthreadJoin(thread, nullptr) == SCE_OK && started.ran.load(), "joining a resumed thread failed"); + Require(raised.load() == 0, "the handler ran for a refused raise"); + Require(sceKernelRemoveExceptionHandler(GUEST_SIGUSR1) == SCE_OK, "sceKernelRemoveExceptionHandler failed"); +} +#else +static void DefersRaiseUntilResume() { + Require(sceKernelInstallExceptionHandler(GUEST_SIGUSR1, reinterpret_cast(&Handler)) == SCE_OK, "sceKernelInstallExceptionHandler failed"); + Started started; + Pthread thread = nullptr; + CreateSuspendedSce(&thread, Record, &started); + Require(sceKernelRaiseException(thread, GUEST_SIGUSR1) == SCE_OK, "sceKernelRaiseException on a suspended thread failed"); + std::this_thread::sleep_for(SUSPENDED_WINDOW); + Require(raised.load() == 0, "the handler ran on a thread still suspended at creation"); + Require(pthread_resume_np_nid_postfix(thread) == 0, "pthread_resume_np failed"); + Require(scePthreadJoin(thread, nullptr) == SCE_OK && started.ran.load(), "joining a resumed thread failed"); + Require(raised.load() == 1 && raisedOn.load() == thread, "the handler did not run on the thread once it was resumed"); + Require(sceKernelRemoveExceptionHandler(GUEST_SIGUSR1) == SCE_OK, "sceKernelRemoveExceptionHandler failed"); +} +#endif + +int main() { + ReportsErrors(); + ResumesOneThread(false); + ResumesOneThread(true); + ResumesRunningAndCallingThreads(); + ResumesAllThreads([] { Require(pthread_multi_np_nid_postfix() == 0, "pthread_multi_np failed"); }); + ResumesAllThreads([] { pthread_resume_all_np_nid_postfix(); }); + ResumesAllThreads([] { Require(scePthreadMulti() == SCE_OK, "scePthreadMulti failed"); }); + ResumesAllThreads([] { scePthreadResumeAll(); }); + CancelsBeforeResume(); + CancelsInResumeAll(); + ReportsCreateSuspendedInAttrGet(pthread_attr_get_np_nid_postfix); + ReportsCreateSuspendedInAttrGet(scePthreadAttrGet); + ResumesDetachedThreadsWhileTheyExit(); +#ifdef _WIN32 + RefusesRaiseWhileSuspended(); +#else + DefersRaiseUntilResume(); +#endif +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index fd41607f44..ee2381b5d5 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -184,6 +184,7 @@ - [pthread_sigmask](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) shares the process-wide mask of `sigprocmask` and `_sigprocmask`, which keeps only the first 32 signals; FreeBSD keeps a mask per thread - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) reaches a thread that waits in an alertable `TimedWait` wait through an APC. A thread blocked anywhere else, including all waits under `APS5_COARSE_TIMED_WAITS` and host waits inside the emulator, is redirected and runs the handler only when that wait returns - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs the handler wherever the target thread was interrupted. It waits only while the thread is inside winpthreads or restoring a context through `NtContinue`, and throws if that lasts 1 s, so a handler that calls into other host code the thread was interrupted in, such as the C runtime's heap or a lock of the emulator, can deadlock on it or reenter it. A thread that holds the address-wait lock of `sceKernelSyncOnAddressWait`, `sceKernelSyncOnAddressWake` and `_umtx_op`, or the park queue lock of the guest mutexes, gets the raise as an APC instead, which runs once it has released the lock or while it waits; the other locks of the emulator are not deferred this way +- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows throws for a target thread that was created suspended and is not resumed yet. FreeBSD 11.4 libthr starts such a thread with every signal but `SIGTRAP` blocked ([`thr_create.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_create.c#L177-L183)) and restores its mask only once it is resumed ([`thread_start`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_create.c#L252-L294)), so a signal sent to it stays pending until then. The console's behaviour is unverified - [sigaltstack](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records each thread's alternate signal stack and reports it back, following FreeBSD's `kern_sigaltstack` (`EINVAL` for flags other than `SS_DISABLE`, `ENOMEM` below `MINSIGSTKSZ` 2048), but handlers never run on it, so `SS_ONSTACK` and `EPERM` are never reported - [sigpending](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) reports the intersection of the process-wide pending set of `sigtimedwait` and the blocked mask of `_sigprocmask`, as FreeBSD's `sys_sigpending` intersects the pending list with `td_sigmask`, and returns -1 with `EFAULT` for a null set. PPSA99420 imports it - [scePthreadJoin, scePthreadCreate](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - a joined thread is kept until `scePthreadCreate` publishes over the same handle storage, so one host object and its handle are retained per distinct guest storage location for the life of the process; a guest that joins into a fresh location every time keeps every one. A handle copied to a second storage and joined there retains the object under the original storage key, so a later `scePthreadCreate` over the original storage releases an object the copy still points at, and the copy reads freed memory. A detached thread is released when it ends, so joining it after it ended reads freed memory. A null entry crashed the payload and throws. Joining the calling thread returns `EDEADLK` and a zeroed attribute creates a default thread, as on the console. PPSA34015 once stopped with `Joining guest thread: Access is denied` before the join and detach checks followed the console diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 5f015be839..74eb0e61c5 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -21,6 +21,7 @@ - [aio_read, aio_write, aio_fsync, aio_error, aio_return, aio_suspend, aio_waitcomplete, aio_cancel](../../../core/libs/prx/libkernel_sys/Aio.cpp) (libkernel_sys) - a request with `SIGEV_SIGNAL`, `SIGEV_THREAD_ID` or `SIGEV_KEVENT` completion, or on a socket, throws, because delivering a guest signal or kevent and socket aio are not modelled. One worker thread runs the process's requests one at a time in submission order, so a request is cancellable only until that thread takes it. A queued request keeps the descriptor number, not the open file: closing or reusing the descriptor before it runs changes what it reaches. Pipes fail with `EOPNOTSUPP`, as FreeBSD 11 does with `vfs.aio.enable_unsafe` 0; the console's setting is unverified. Waits are not interrupted by signals (`EINTR`), and timeouts under 1 ms round up to 1 ms (hz 1000). Otherwise the rules follow FreeBSD 11 `vfs_aio.c` - [readlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `readlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified - [link](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows resolves a symbolic link source through the host, which follows longer chains than FreeBSD: a chain of 40 links is linked, where FreeBSD 11.4 returns `ELOOP` after 32 (`MAXSYMLINKS`). A loop returns `ELOOP` as on FreeBSD +- [pthread_attr_setcreatesuspend_np, pthread_resume_np, pthread_resume_all_np, pthread_multi_np, scePthreadAttrSetcreatesuspend, scePthreadResume, scePthreadResumeAll and scePthreadMulti](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) - when a thread created suspended stops relative to its signal mask is unmeasured on a console. Return values and errors follow FreeBSD 11.4 libthr ([`thr_attr.c`](https://github.com/freebsd/freebsd-src/blob/8c639945/lib/libthr/thread/thr_attr.c#L369-L381), [`thr_resume_np.c`](https://github.com/freebsd/freebsd-src/blob/8c639945/lib/libthr/thread/thr_resume_np.c#L47-L97), [`thr_multi_np.c`](https://github.com/freebsd/freebsd-src/blob/8c639945/lib/libthr/thread/thr_multi_np.c#L40-L51)) and the code of firmware 13.42's `libkernel_sys.sprx` ([#9573](https://github.com/boykopovar/AnyPS5/pull/9573#issuecomment-6393877843)), where the sce forms add `0x80020000` to a non-zero errno - [fstatat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `stat`, or to `lstat` with `AT_SYMLINK_NOFOLLOW` (0x200). Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory, and flags other than `AT_SYMLINK_NOFOLLOW` return `EINVAL`. PPSA74126 imports it. Console behaviour is unverified - [unlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rmdir` (with `AT_REMOVEDIR`, 0x800) or `unlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Removing a directory without `AT_REMOVEDIR` fails with `EPERM`, removing `.` or `..` with `AT_REMOVEDIR` fails with `EINVAL`, and flags other than `AT_REMOVEDIR` fail with `EINVAL`. Console behaviour is unverified - [renameat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rename`. Any other relative path is joined to its descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to relative descriptors, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified From e2096fa49a473d12016092de8bcb7509c84c9902 Mon Sep 17 00:00:00 2001 From: ntamas94 Date: Thu, 22 Oct 2026 07:42:55 +0200 Subject: [PATCH 407/428] feat(libSceAjm): run PS4 batch buffer jobs in the console's chunk layout PS4 titles build AJM batches with the SDK's inline helpers, which call sceAjmBatchJobControlBufferRa, sceAjmBatchJobRunBufferRa and sceAjmBatchJobRunSplitBufferRa and start the buffer with sceAjmBatchStartBuffer. The buffer belongs to the title, so it has to use the console's chunk layout rather than this library's private records. The builders write console jobs (job, return address, buffer and flags chunks) and return the position after them. StartBuffer checks the context, walks the chunks, skips inline data and runs each job as the library's existing control, run or statistics job. Chunk headers are read as the console's builders write them: bits 0-3 the kind, bit 4 a buffer's output direction, bits 6-25 the payload; the other bits keep a reused buffer's old bytes on a PS4. Malformed batches return MALFORMED_BATCH, unknown chunk kinds and unimplemented control flags throw. An ATRAC9 run with RUN_MULTIPLE_FRAMES reports which side ran out as a PS4 does (0x8 input, 0x10 room, 0x18 both), and the ATRAC9 format reports the frame's bitrate; both were measured on a PS4 with Knowledge is Power: Decades (Wwise) batch dumps and probe runs. The vibration test's exact-fit run expects 0x18 accordingly. The throwing stubs in libSceAjm and the libSceAjmi copy of sceAjmBatchJobRunBufferRa are dropped. Co-Authored-By: Claude Opus 5.5 Co-Authored-By: Claude Fable 5.1 --- .../libs/prx/libSceAjm.native/include/Api.hpp | 4 + core/libs/prx/libSceAjm.native/src/Ajm.cpp | 217 ++++++++++++++++- core/libs/prx/libSceAjm/Export.cpp | 15 +- core/libs/prx/libSceAjmi/Export.cpp | 5 - core/libs/tests/GuestAjm.cpp | 227 +++++++++++++++++- core/libs/tests/GuestAjmVibration.cpp | 2 +- docs/dev/debt/libSceAjm.native.md | 6 +- 7 files changed, 438 insertions(+), 38 deletions(-) diff --git a/core/libs/prx/libSceAjm.native/include/Api.hpp b/core/libs/prx/libSceAjm.native/include/Api.hpp index 3c1f8608ed..6547522846 100644 --- a/core/libs/prx/libSceAjm.native/include/Api.hpp +++ b/core/libs/prx/libSceAjm.native/include/Api.hpp @@ -70,5 +70,9 @@ int APS5_VABI sceAjmBatchStart(uint32_t context, const AjmBatchInfo* info, int p int APS5_VABI sceAjmBatchWait(uint32_t context, uint32_t batch, uint32_t timeout, AjmBatchError* error); int APS5_VABI sceAjmBatchCancel(uint32_t context, uint32_t batch); int APS5_VABI sceAjmBatchErrorDump(const AjmBatchInfo* info, AjmBatchError* error); +void* APS5_VABI sceAjmBatchJobControlBufferRa(void* position, uint32_t instance, uint64_t flags, const void* sideband_input, size_t sideband_input_size, void* sideband_output, size_t sideband_output_size, void* return_address); +void* APS5_VABI sceAjmBatchJobRunBufferRa(void* position, uint32_t instance, uint64_t flags, const void* data_input, size_t data_input_size, void* data_output, size_t data_output_size, void* sideband_output, size_t sideband_output_size, void* return_address); +void* APS5_VABI sceAjmBatchJobRunSplitBufferRa(void* position, uint32_t instance, uint64_t flags, const AjmBuffer* input_buffers, size_t input_buffers_num, const AjmBuffer* output_buffers, size_t output_buffers_num, void* sideband_output, size_t sideband_output_size, void* return_address); +int APS5_VABI sceAjmBatchStartBuffer(uint32_t context, const uint8_t* batch, uint32_t batch_size, int priority, AjmBatchError* error, uint32_t* batch_id); } diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 195ee36a3a..41baae55e0 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -36,6 +36,7 @@ constexpr int SCE_AJM_ERROR_INVALID_INSTANCE = static_cast(0x80930003); constexpr int SCE_AJM_ERROR_INVALID_BATCH = static_cast(0x80930004); constexpr int SCE_AJM_ERROR_INVALID_PARAMETER = static_cast(0x80930005); constexpr int SCE_AJM_ERROR_OUT_OF_RESOURCES = static_cast(0x80930007); +constexpr int SCE_AJM_ERROR_MALFORMED_BATCH = static_cast(0x80930011); constexpr std::int32_t AJM_RESULT_NOT_INITIALIZED = 0x00000001; constexpr std::int32_t AJM_RESULT_INVALID_DATA = 0x00000002; @@ -691,9 +692,17 @@ void EmitFrame(Instance& instance, PcmOutputs& outputs, const std::uint8_t* pcm, ResamplerProduce(instance, outputs, channels, encoding, sampleBytes); } +// A PS4 reports an ATRAC9 stream's bitrate from its superframe: 448-byte superframes of 4 frames of 256 +// samples at 48 kHz gave 168000 (Knowledge is Power: Decades, every run job's format sideband). +std::uint32_t At9Bitrate(const Instance& instance) { + const auto samples = static_cast(instance.info.framesInSuperframe) * static_cast(instance.info.frameSamples); + if (samples == 0) return 0; + return static_cast(static_cast(instance.info.superframeSize) * 8u * static_cast(instance.info.samplingRate) / samples); +} + SidebandFormat CurrentFormat(const Instance& instance) { switch (instance.codec) { - case CODEC_AT9: return {static_cast(instance.info.channels), ChannelMask(static_cast(instance.info.channels)), static_cast(instance.info.samplingRate), PcmEncoding(instance), 0, 0}; + case CODEC_AT9: return {static_cast(instance.info.channels), ChannelMask(static_cast(instance.info.channels)), static_cast(instance.info.samplingRate), PcmEncoding(instance), At9Bitrate(instance), 0}; case CODEC_MP3: return {instance.mp3Channels, ChannelMask(instance.mp3Channels), instance.mp3SampleRate, PcmEncoding(instance), instance.mp3Bitrate, 0}; case CODEC_OPUS: return {instance.opusChannels, ChannelMask(instance.opusChannels), instance.opusSampleRate, PcmEncoding(instance), 0, 0}; case CODEC_M4AAC: return {instance.aacChannels, ChannelMask(instance.aacChannels), instance.aacSampleRate, PcmEncoding(instance), instance.aacBitrate, 0}; @@ -768,12 +777,22 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c if (segmentEnded()) break; if (instance.superframeRemaining == 0) consumed += RiffDataOffset(input.data() + consumed, input.size() - consumed); const std::size_t needed = instance.superframeRemaining == 0 ? superframeSize : instance.superframeRemaining; + const bool roomForFrame = Resampling(instance) ? pcmOutputs.Room() >= channels * sampleBytes : pcmOutputs.Room() >= frameBytes; + // A PS4 reports which side ran out (probe runs of a title's ATRAC9 stream through libSceAjm's + // builders): PARTIAL_INPUT when the input ran out, on a superframe boundary or not, with room + // left; NOT_ENOUGH_ROOM when the room ran out with input left, after however many frames (and + // with none decoded for a zero room); both (0x18) when the input ended just as the output + // filled, which is every Wwise run job. A run without input (a format query) reports 0. + // Resampled runs were not measured and keep the earlier rule: a trailing partial superframe is + // partial input, a zero-output run is out of room. + const bool measured = !Resampling(instance); if (input.size() - consumed < needed) { - if (input.size() != consumed) result |= AJM_RESULT_PARTIAL_INPUT; + if (input.size() != consumed || (measured && !input.empty())) result |= AJM_RESULT_PARTIAL_INPUT; + if (measured && !input.empty() && !roomForFrame) result |= AJM_RESULT_NOT_ENOUGH_ROOM; break; } - if (Resampling(instance) ? pcmOutputs.Room() < channels * sampleBytes : pcmOutputs.Room() < frameBytes) { - if (frames == 0 && pcmOutputs.produced == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; + if (!roomForFrame) { + if (measured || (frames == 0 && pcmOutputs.produced == 0)) result |= AJM_RESULT_NOT_ENOUGH_ROOM; break; } if (instance.superframeRemaining == 0) { @@ -1692,6 +1711,137 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out } } +int RegisterBatch(std::uint32_t context, AjmBatchError* error, std::uint32_t* batch) { + if (error) std::memset(error, 0, sizeof(*error)); + const auto id = g_nextBatch.fetch_add(1, std::memory_order_relaxed); + { + std::lock_guard lock(g_batchLock); + g_batches.emplace(id, context); + } + *batch = id; + return 0; +} + +// The PS4 batch buffer (sceAjmBatchStartBuffer) is not private: the PS4 SDK builds it with inline code +// in the title, so it has the console's layout, taken from shadPS4's reimplementation of the PS4 +// library. It is a run of chunks, each starting with a 32-bit word that holds the chunk kind in bits +// 0-5 and a payload in bits 6-25. A job chunk has the instance as payload and, in a second word, the +// byte count of the chunks after it that describe the job; an inline chunk holds that many bytes of data +// a job's control buffer may point to. A job's flags chunk has flags bits 32-51 as payload and bits 0-31 +// in its second word; its buffer chunks have a 32-bit size and a 64-bit address. +enum class ConsoleChunk : std::uint32_t { + Job = 0, + InputRunBuffer = 1, + InputControlBuffer = 2, + ControlFlags = 3, + RunFlags = 4, + ReturnAddress = 6, + InlineBuffer = 7, + OutputRunBuffer = 17, + OutputControlBuffer = 18, +}; + +constexpr std::uint32_t CONSOLE_STATISTICS_INSTANCE = 0x80000; +constexpr std::uint64_t CONSOLE_FLAGS_VERSION_AND_CODEC = 0x7FF; + +// The low header word as the PS4's libSceAjm builders write it (a trace of 32 545 batches a Wwise title +// started on a PS4): bits 0-3 the kind, bit 4 the output direction of a buffer chunk +// (0x11 output run, 0x12 output control), bits 6-25 the job's instance or the flags' bits 32-51. The +// builders leave bits 5, 26, 27 and 31 as they were, and a title that reuses its batch memory (Wwise +// does) carries the previous batch's bytes there: a 6-bit kind read job headers as 16, 32 or 48 and run +// flags as 20, 36 or 52, and a 26-bit payload read flags bit 52 set. +ConsoleChunk ConsoleChunkKind(std::uint32_t word) { + const std::uint32_t kind = word & 0xFu; + return static_cast(kind == 1 || kind == 2 ? kind | (word & 0x10u) : kind); +} + +std::uint32_t ConsoleChunkPayload(std::uint32_t word) { + return (word >> 6) & 0xFFFFFu; +} + +std::uint8_t* PutConsoleChunk(std::uint8_t* at, ConsoleChunk kind, std::uint32_t payload, std::uint32_t word) { + const std::uint32_t words[2] = {static_cast(kind) | ((payload & 0xFFFFFu) << 6), word}; + std::memcpy(at, words, sizeof(words)); + return at + sizeof(words); +} + +std::uint8_t* PutConsoleFlags(std::uint8_t* at, ConsoleChunk kind, std::uint64_t flags) { + return PutConsoleChunk(at, kind, static_cast(flags >> 32), static_cast(flags)); +} + +std::uint8_t* PutConsoleBuffer(std::uint8_t* at, ConsoleChunk kind, const void* address, std::size_t size) { + at = PutConsoleChunk(at, kind, 0, static_cast(size)); + std::memcpy(at, &address, sizeof(address)); + return at + sizeof(address); +} + +std::uint8_t* BeginConsoleJob(std::uint8_t* job, const void* returnAddress) { + std::uint8_t* at = job + 2 * sizeof(std::uint32_t); + return returnAddress ? PutConsoleBuffer(at, ConsoleChunk::ReturnAddress, returnAddress, 0) : at; +} + +void* EndConsoleJob(std::uint8_t* job, std::uint8_t* end, std::uint32_t instance) { + PutConsoleChunk(job, ConsoleChunk::Job, instance, static_cast(end - job) - 2 * sizeof(std::uint32_t)); + return end; +} + +// Runs one console job as the job of this file's kind it describes. The return address is only kept by +// the console for error reports. Returns false for a job whose chunks overrun it or that has no flags. +bool RunConsoleJob(std::uint32_t instance, const std::uint8_t* at, const std::uint8_t* end) { + JobHeader job{}; + job.instance = instance; + std::vector inputs; + std::vector outputs; + AjmBuffer controlInput{}; + AjmBuffer controlOutput{}; + ConsoleChunk flagsKind = ConsoleChunk::Job; + while (at < end) { + if (end - at < 8) return false; + std::uint32_t words[2]; + std::memcpy(words, at, sizeof(words)); + const auto kind = ConsoleChunkKind(words[0]); + if (kind == ConsoleChunk::ControlFlags || kind == ConsoleChunk::RunFlags) { + if (flagsKind != ConsoleChunk::Job) return false; + flagsKind = kind; + job.flags = (std::uint64_t{ConsoleChunkPayload(words[0])} << 32) | words[1]; + at += sizeof(words); + continue; + } + if (kind != ConsoleChunk::InputRunBuffer && kind != ConsoleChunk::InputControlBuffer && kind != ConsoleChunk::ReturnAddress && kind != ConsoleChunk::OutputRunBuffer && kind != ConsoleChunk::OutputControlBuffer) { + NotImplemented_nid_no_patch(("sceAjmBatchStartBuffer (job chunk kind " + std::to_string(static_cast(kind)) + ")").c_str()); + } + if (end - at < 16) return false; + AjmBuffer buffer{nullptr, words[1]}; + std::memcpy(&buffer.ptr, at + sizeof(words), sizeof(buffer.ptr)); + at += 16; + if (kind == ConsoleChunk::InputRunBuffer && buffer.size) inputs.push_back(buffer); + if (kind == ConsoleChunk::OutputRunBuffer && buffer.ptr && buffer.size) outputs.push_back(buffer); + if (kind == ConsoleChunk::InputControlBuffer) controlInput = buffer; + if (kind == ConsoleChunk::OutputControlBuffer) controlOutput = buffer; + } + if (flagsKind == ConsoleChunk::Job) return false; + job.sideband = controlOutput.ptr; + job.sidebandSize = controlOutput.size; + if (instance == CONSOLE_STATISTICS_INSTANCE) { + job.kind = JobKind::GetStatistics; + } else if (flagsKind == ConsoleChunk::ControlFlags) { + constexpr std::uint64_t implemented = CONSOLE_FLAGS_VERSION_AND_CODEC | CONTROL_RESET | CONTROL_INITIALIZE | SIDEBAND_GAPLESS_DECODE; + if ((job.flags & ~implemented) != 0) NotImplemented_nid_no_patch("sceAjmBatchStartBuffer (control job flags other than RESET, INITIALIZE and SIDEBAND_GAPLESS_DECODE)"); + if (!inputs.empty() || !outputs.empty()) NotImplemented_nid_no_patch("sceAjmBatchStartBuffer (control job with run buffers)"); + job.kind = JobKind::Control; + if (controlInput.size) inputs.push_back(controlInput); + } else { + if (controlInput.size) NotImplemented_nid_no_patch("sceAjmBatchStartBuffer (run job with an input control buffer)"); + job.kind = JobKind::Run; + } + job.inputCount = static_cast(inputs.size()); + job.outputCount = static_cast(outputs.size()); + AJM_TRACE("[ajm] console job: instance %u, %s flags 0x%llx, %u input and %u output buffers, control input %llu and output %llu bytes\n", instance, flagsKind == ConsoleChunk::ControlFlags ? "control" : "run", static_cast(job.flags), job.inputCount, job.outputCount, static_cast(controlInput.size), static_cast(controlOutput.size)); + inputs.insert(inputs.end(), outputs.begin(), outputs.end()); + Execute(job, inputs.data(), inputs.data() + job.inputCount); + return true; +} + } extern "C" { @@ -1923,14 +2073,7 @@ int APS5_VABI sceAjmBatchStart(uint32_t context, const AjmBatchInfo* info, int p remaining -= record.bytes; if (remaining != 0) cursor += record.bytes; } - if (error) std::memset(error, 0, sizeof(*error)); - const auto id = g_nextBatch.fetch_add(1, std::memory_order_relaxed); - { - std::lock_guard lock(g_batchLock); - g_batches.emplace(id, context); - } - *batch = id; - return 0; + return RegisterBatch(context, error, batch); } int APS5_VABI sceAjmBatchWait(uint32_t context, uint32_t batch, uint32_t timeout, AjmBatchError* error) { @@ -1959,4 +2102,54 @@ int APS5_VABI sceAjmBatchErrorDump(const AjmBatchInfo* info, AjmBatchError* erro return 0; } +void* APS5_VABI sceAjmBatchJobControlBufferRa(void* position, uint32_t instance, uint64_t flags, const void* sideband_input, size_t sideband_input_size, void* sideband_output, size_t sideband_output_size, void* return_address) { + auto* job = static_cast(position); + auto* at = BeginConsoleJob(job, return_address); + at = PutConsoleBuffer(at, ConsoleChunk::InputControlBuffer, sideband_input, sideband_input_size); + at = PutConsoleFlags(at, ConsoleChunk::ControlFlags, flags); + at = PutConsoleBuffer(at, ConsoleChunk::OutputControlBuffer, sideband_output, sideband_output_size); + return EndConsoleJob(job, at, instance); +} + +void* APS5_VABI sceAjmBatchJobRunSplitBufferRa(void* position, uint32_t instance, uint64_t flags, const AjmBuffer* input_buffers, size_t input_buffers_num, const AjmBuffer* output_buffers, size_t output_buffers_num, void* sideband_output, size_t sideband_output_size, void* return_address) { + if ((input_buffers_num && !input_buffers) || (output_buffers_num && !output_buffers)) NotImplemented_nid_no_patch("AJM batch job with a null buffer list and a nonzero buffer count"); + auto* job = static_cast(position); + auto* at = BeginConsoleJob(job, return_address); + for (std::size_t index = 0; index < input_buffers_num; ++index) at = PutConsoleBuffer(at, ConsoleChunk::InputRunBuffer, input_buffers[index].ptr, input_buffers[index].size); + at = PutConsoleFlags(at, ConsoleChunk::RunFlags, flags); + for (std::size_t index = 0; index < output_buffers_num; ++index) at = PutConsoleBuffer(at, ConsoleChunk::OutputRunBuffer, output_buffers[index].ptr, output_buffers[index].size); + at = PutConsoleBuffer(at, ConsoleChunk::OutputControlBuffer, sideband_output, sideband_output_size); + return EndConsoleJob(job, at, instance); +} + +void* APS5_VABI sceAjmBatchJobRunBufferRa(void* position, uint32_t instance, uint64_t flags, const void* data_input, size_t data_input_size, void* data_output, size_t data_output_size, void* sideband_output, size_t sideband_output_size, void* return_address) { + const AjmBuffer input{const_cast(data_input), data_input_size}; + const AjmBuffer output{data_output, data_output_size}; + return sceAjmBatchJobRunSplitBufferRa(position, instance, flags, &input, 1, &output, 1, sideband_output, sideband_output_size, return_address); +} + +int APS5_VABI sceAjmBatchStartBuffer(uint32_t context, const uint8_t* batch, uint32_t batch_size, int priority, AjmBatchError* error, uint32_t* batch_id) { + if (context == 0 || context >= g_nextContext.load(std::memory_order_relaxed)) return SCE_AJM_ERROR_INVALID_CONTEXT; + if (!batch_id || (!batch && batch_size)) return SCE_AJM_ERROR_INVALID_PARAMETER; + AJM_TRACE("[ajm] batch start buffer: context %u, priority %d, %u bytes\n", context, priority, batch_size); + if (batch_size % 8 != 0) return SCE_AJM_ERROR_MALFORMED_BATCH; + const std::uint8_t* at = batch; + const std::uint8_t* end = batch + batch_size; + while (at < end) { + if (end - at < 8) return SCE_AJM_ERROR_MALFORMED_BATCH; + std::uint32_t words[2]; + std::memcpy(words, at, sizeof(words)); + const std::uint8_t* body = at + sizeof(words); + if (words[1] > static_cast(end - body)) return SCE_AJM_ERROR_MALFORMED_BATCH; + const auto kind = ConsoleChunkKind(words[0]); + if (kind == ConsoleChunk::Job) { + if (!RunConsoleJob(ConsoleChunkPayload(words[0]), body, body + words[1])) return SCE_AJM_ERROR_MALFORMED_BATCH; + } else if (kind != ConsoleChunk::InlineBuffer) { + NotImplemented_nid_no_patch(("sceAjmBatchStartBuffer (batch chunk kind " + std::to_string(static_cast(kind)) + ")").c_str()); + } + at = body + words[1]; + } + return RegisterBatch(context, error, batch_id); +} + } diff --git a/core/libs/prx/libSceAjm/Export.cpp b/core/libs/prx/libSceAjm/Export.cpp index be23cfdc39..87830816c5 100644 --- a/core/libs/prx/libSceAjm/Export.cpp +++ b/core/libs/prx/libSceAjm/Export.cpp @@ -6,19 +6,6 @@ extern "C" { -int APS5_VABI sceAjmBatchJobControlBufferRa() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAjmBatchJobRunSplitBufferRa() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAjmBatchStartBuffer() { - NotImplemented_nid_no_patch(__func__); - return 0; -} +APS5_DUMMY_FUN } diff --git a/core/libs/prx/libSceAjmi/Export.cpp b/core/libs/prx/libSceAjmi/Export.cpp index 3941ee53aa..7932a6f3c8 100644 --- a/core/libs/prx/libSceAjmi/Export.cpp +++ b/core/libs/prx/libSceAjmi/Export.cpp @@ -13,9 +13,4 @@ int APS5_VABI sceAjmBatchJobInlineBuffer(void) { return 0; } -int APS5_VABI sceAjmBatchJobRunBufferRa(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 6032301a75..f850b844ff 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -825,7 +825,7 @@ void TestGetInfo(std::uint32_t context) { Require(sceAjmBatchJobGetInfo(&info, instance, &at9) == 0); Submit(context, info); Require(initResult[0] == 0); - Require(at9.result == 0 && at9.internalResult == 0 && at9.numChannels == 2 && at9.channelMask == 0x3 && at9.sampleRate == 48000 && at9.sampleEncoding == 0 && at9.reserved == 0); + Require(at9.result == 0 && at9.internalResult == 0 && at9.numChannels == 2 && at9.channelMask == 0x3 && at9.sampleRate == 48000 && at9.sampleEncoding == 0 && at9.bitrate == 1024 * 8 * 48000 / (4 * 256) && at9.reserved == 0); Require(sceAjmInstanceDestroy(context, instance) == 0); Require(sceAjmInstanceCreate(context, 0, 0, &instance) == 0); @@ -1051,7 +1051,8 @@ std::vector DecodeAt9Superframe(std::uint32_t context, const std:: Require(sceAjmBatchJobRun(&info, instance, sidebandStream | runMultipleFrames, superframe.data(), superframe.size(), pcm.data(), pcm.size() * sizeof(std::int16_t), &decoded, sizeof(decoded)) == 0); Submit(context, info); Require(initResult[0] == 0); - Require(decoded.stream.result == 0 && static_cast(decoded.stream.inputConsumed) == superframe.size()); + // one superframe into exactly its 1024 samples: PARTIAL_INPUT | NOT_ENOUGH_ROOM as on the console + Require(decoded.stream.result == 0x18 && static_cast(decoded.stream.inputConsumed) == superframe.size()); Require(static_cast(decoded.stream.outputWritten) == pcm.size() * sizeof(std::int16_t) && decoded.stream.totalDecodedSamples == 1024 && decoded.frames == 4); Require(sceAjmInstanceDestroy(context, instance) == 0); return pcm; @@ -1578,7 +1579,8 @@ ResampledStream Stream(std::uint32_t context, std::uint32_t instance, const std: Require(sceAjmBatchJobDecode(&info, instance, stream + offset, size - offset, pcm.data(), pcm.size() * sizeof(std::int16_t), &sideband) == 0); Submit(context, info); if (ratio > 0) Require(setResult[0] == 0); - Require(sideband.result == 0 || (offset == size && sideband.outputWritten == 0)); + // 0x8 / 0x10 / 0x18: which of input and room ran out (what the console reports, see TestAt9FlagRules) + Require(sideband.result == 0 || sideband.result == 0x8 || sideband.result == 0x10 || sideband.result == 0x18 || (offset == size && sideband.outputWritten == 0)); offset += static_cast(sideband.inputConsumed); if (offset < size && static_cast(sideband.outputWritten) < pcm.size() * sizeof(std::int16_t)) ++out.shortJobs; out.pcm.insert(out.pcm.end(), pcm.begin(), pcm.begin() + sideband.outputWritten / static_cast(sizeof(std::int16_t))); @@ -1798,7 +1800,7 @@ At9Run RunAt9Job(std::uint32_t context, std::uint32_t instance, std::uint64_t fl const std::size_t sidebandSize = (flags & runMultipleFrames) != 0 ? sizeof(decoded) : sizeof(decoded.stream); Require(sceAjmBatchJobRun(&info, instance, sidebandStream | flags, input, inputSize, pcm.data(), pcm.size() * sizeof(std::int16_t), &decoded, sidebandSize) == 0); Submit(context, info); - Require(decoded.stream.result == 0); + Require(decoded.stream.result == 0 || decoded.stream.result == 0x8 || decoded.stream.result == 0x18); // which side ran out, see TestAt9FlagRules return decoded; } @@ -1832,7 +1834,218 @@ void TestAt9RunDecodesWholeInput(std::uint32_t context) { RunControl(context, instance, CONTROL_START, &start, sizeof(start), result); Require(result[0] == 0); const auto run = RunAt9Job(context, instance, 0, stream.data(), stream.size()); - Require(static_cast(run.stream.inputConsumed) == stream.size() && run.stream.outputWritten == 1024 * 2); + // 1024 bytes decode into exactly the 1024 samples of room: the console reports PARTIAL_INPUT | NOT_ENOUGH_ROOM + Require(run.stream.result == 0x18 && static_cast(run.stream.inputConsumed) == stream.size() && run.stream.outputWritten == 1024 * 2); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +// The result flags say which side ran out, as a PS4 reports them for a title's ATRAC9 stream: the input +// (PARTIAL_INPUT, even on a superframe boundary) with room left, the room (NOT_ENOUGH_ROOM) with input +// left after some frames or with a zero room before any, or both at once (0x18). +void TestAt9FlagRules(std::uint32_t context) { + constexpr std::uint64_t runMultipleFrames = 1ull << 12; + constexpr std::uint64_t sidebandStream = 1ull << 47; + std::vector stream(2048, 0); + std::memcpy(stream.data(), AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME)); + std::memcpy(stream.data() + 1024, AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME)); + const At9Control start{{0, 0, 0}, {0xFE, 0x70, 0x1F, 0xF0}, 0}; + const auto run = [&](std::size_t inputSize, std::size_t roomSamples, At9Run& decoded) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0); + std::int32_t result[2] = {-1, -1}; + RunControl(context, instance, CONTROL_START, &start, sizeof(start), result); + Require(result[0] == 0); + std::vector batch(4096); + std::vector pcm(roomSamples + 1); + AjmBatchInfo info{}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobRun(&info, instance, sidebandStream | runMultipleFrames, stream.data(), inputSize, pcm.data(), roomSamples * sizeof(std::int16_t), &decoded, sizeof(decoded)) == 0); + Submit(context, info); + Require(sceAjmInstanceDestroy(context, instance) == 0); + }; + At9Run inputRanOut{{-1, -1, 0, 0, 0}, 0, 0}; + run(1024, 2048, inputRanOut); + Require(inputRanOut.stream.result == 0x8 && inputRanOut.stream.inputConsumed == 1024 && inputRanOut.stream.outputWritten == 1024 * 2 && inputRanOut.frames == 4); + At9Run roomRanOut{{-1, -1, 0, 0, 0}, 0, 0}; + run(2048, 1024, roomRanOut); + Require(roomRanOut.stream.result == 0x10 && roomRanOut.stream.inputConsumed == 1024 && roomRanOut.stream.outputWritten == 1024 * 2 && roomRanOut.frames == 4); + At9Run halfRoom{{-1, -1, 0, 0, 0}, 0, 0}; + run(1024, 512, halfRoom); + Require(halfRoom.stream.result == 0x10 && halfRoom.stream.inputConsumed > 0 && halfRoom.stream.inputConsumed < 1024 && halfRoom.stream.outputWritten == 512 * 2 && halfRoom.frames == 2); + At9Run noRoom{{-1, -1, 0, 0, 0}, 0, 0}; + run(1024, 0, noRoom); + Require(noRoom.stream.result == 0x10 && noRoom.stream.inputConsumed == 0 && noRoom.stream.outputWritten == 0 && noRoom.frames == 0); + At9Run both{{-1, -1, 0, 0, 0}, 0, 0}; + run(1024, 1024, both); + Require(both.stream.result == 0x18 && both.stream.inputConsumed == 1024 && both.stream.outputWritten == 1024 * 2 && both.frames == 4); +} + +int StartBuffer(std::uint32_t context, const void* batch, std::size_t size) { + std::uint32_t id = 0; + AjmBatchError error{}; + const int started = sceAjmBatchStartBuffer(context, static_cast(batch), static_cast(size), 0, &error, &id); + if (started == 0) Require(sceAjmBatchWait(context, id, 0, &error) == 0); + return started; +} + +std::uint32_t BatchWord(const std::vector& batch, std::size_t offset) { + std::uint32_t word = 0; + std::memcpy(&word, batch.data() + offset, sizeof(word)); + return word; +} + +void TestConsoleBufferJobs(std::uint32_t context) { + constexpr std::uint64_t version = 1; + constexpr std::uint64_t runMultipleFrames = 1ull << 12; + constexpr std::uint64_t sidebandStream = 1ull << 47; + std::vector stream(2048, 0); + std::memcpy(stream.data(), AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME)); + std::memcpy(stream.data() + 1024, AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME)); + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0); + const At9Control start{{0, 0, 0}, {0xFE, 0x70, 0x1F, 0xF0}, 0}; + std::int32_t result[2] = {-1, -1}; + std::vector pcm(2048, 0x55); + At9Run single{{-1, -1, 0, 0, 0}, 0, 0}; + At9Run split{{-1, -1, 0, 0, 0}, 0, 0}; + const AjmBuffer inputs[2] = {{stream.data() + 1024, 300}, {stream.data() + 1324, 724}}; + const AjmBuffer outputs[2] = {{pcm.data() + 1024, 600 * 2}, {pcm.data() + 1624, 424 * 2}}; + std::vector batch(4096, 0xCD); + + void* at = sceAjmBatchJobControlBufferRa(batch.data(), instance, version | CONTROL_START, &start, sizeof(start), result, sizeof(result), &context); + Require(at == batch.data() + 64); + Require(BatchWord(batch, 0) == instance << 6 && BatchWord(batch, 4) == 56); + Require(BatchWord(batch, 8) == 6 && BatchWord(batch, 24) == 2 && BatchWord(batch, 28) == sizeof(start)); + Require(BatchWord(batch, 40) == (3u | (0x2000u << 6)) && BatchWord(batch, 44) == 0x6001 && BatchWord(batch, 48) == 18); + at = sceAjmBatchJobRunBufferRa(at, instance, version | sidebandStream | runMultipleFrames, stream.data(), 1024, pcm.data(), 1024 * 2, &single, sizeof(single), nullptr); + Require(at == batch.data() + 128); + Require(BatchWord(batch, 68) == 56 && BatchWord(batch, 72) == 1 && BatchWord(batch, 88) == (4u | (0x8000u << 6)) && BatchWord(batch, 96) == 17 && BatchWord(batch, 112) == 18); + at = sceAjmBatchJobRunSplitBufferRa(at, instance, version | sidebandStream | runMultipleFrames, inputs, 2, outputs, 2, &split, sizeof(split), nullptr); + Require(at == batch.data() + 224 && BatchWord(batch, 132) == 88); + Require(StartBuffer(context, batch.data(), static_cast(at) - batch.data()) == 0); + + Require(result[0] == 0 && result[1] == 0); + // input and output both end with the fourth frame: the console reports PARTIAL_INPUT | NOT_ENOUGH_ROOM + constexpr std::int32_t exactFit = 0x18; + Require(single.stream.result == exactFit && single.stream.inputConsumed == 1024 && single.stream.outputWritten == 1024 * 2 && single.frames == 4 && single.stream.totalDecodedSamples == 1024); + Require(split.stream.result == exactFit && split.stream.inputConsumed == 1024 && split.stream.outputWritten == 1024 * 2 && split.frames == 4 && split.stream.totalDecodedSamples == 2048); + Require(std::all_of(pcm.begin(), pcm.end(), [](std::int16_t sample) { return sample == 0; })); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +class ConsoleBatch { +public: + void Word(std::uint32_t word) { words.push_back(word); } + void Chunk(std::uint32_t kind, std::uint32_t payload, std::uint32_t word) { + Word(kind | (payload << 6)); + Word(word); + } + void Buffer(std::uint32_t kind, const void* address, std::size_t size) { + Chunk(kind, 0, static_cast(size)); + const auto value = static_cast(reinterpret_cast(address)); + Word(static_cast(value)); + Word(static_cast(value >> 32)); + } + const std::uint32_t* Data() const { return words.data(); } + std::size_t Bytes() const { return words.size() * sizeof(std::uint32_t); } + +private: + std::vector words; +}; + +void TestConsoleHandWrittenJobs(std::uint32_t context) { + std::vector stream(1024, 0); + std::memcpy(stream.data(), AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME)); + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0); + std::int32_t result[2] = {-1, -1}; + std::int32_t statistics[6] = {-1, -1, -1, -1, -1, -1}; + std::vector pcm(1024, 0x55); + At9Run decoded{{-1, -1, 0, 0, 0}, 0, 0}; + const std::uint8_t initialize[8] = {0xFE, 0x70, 0x1F, 0xF0, 0, 0, 0, 0}; + std::uint32_t inlined[2]; + std::memcpy(inlined, initialize, sizeof(initialize)); + + ConsoleBatch batch; + batch.Chunk(7, 0, sizeof(initialize)); + batch.Word(inlined[0]); + batch.Word(inlined[1]); + batch.Chunk(0, instance, 40); + batch.Buffer(2, nullptr, sizeof(initialize)); + batch.Chunk(3, 0, 0x6001); + batch.Buffer(18, result, sizeof(result)); + batch.Chunk(0, instance, 56); + batch.Buffer(1, stream.data(), stream.size()); + batch.Chunk(4, 0x8000, 0x1001); + batch.Buffer(17, pcm.data(), pcm.size() * sizeof(std::int16_t)); + batch.Buffer(18, &decoded, sizeof(decoded)); + batch.Chunk(0, 0x80000, 24); + batch.Chunk(3, 0, 0x8007); + batch.Buffer(18, statistics, sizeof(statistics)); + std::vector words(batch.Data(), batch.Data() + batch.Bytes() / sizeof(std::uint32_t)); + const auto inlineAddress = static_cast(reinterpret_cast(words.data() + 2)); + words[8] = static_cast(inlineAddress); + words[9] = static_cast(inlineAddress >> 32); + Require(StartBuffer(context, words.data(), words.size() * sizeof(std::uint32_t)) == 0); + Require(result[0] == 0 && result[1] == 0 && statistics[0] == 0 && statistics[1] == 0); + Require(decoded.stream.result == 0x18 && decoded.stream.inputConsumed == 1024 && decoded.stream.outputWritten == 1024 * 2 && decoded.frames == 4); + Require(std::all_of(pcm.begin(), pcm.end(), [](std::int16_t sample) { return sample == 0; })); + + constexpr int malformedBatch = static_cast(0x80930011); + Require(StartBuffer(context, words.data(), 12) == malformedBatch); + ConsoleBatch flagless; + flagless.Chunk(0, instance, 16); + flagless.Buffer(18, result, sizeof(result)); + Require(StartBuffer(context, flagless.Data(), flagless.Bytes()) == malformedBatch); + ConsoleBatch overrun; + overrun.Chunk(0, instance, 32); + overrun.Chunk(3, 0, 0x2001); + overrun.Buffer(18, result, sizeof(result)); + Require(StartBuffer(context, overrun.Data(), overrun.Bytes()) == malformedBatch); + ConsoleBatch unknown; + unknown.Chunk(0, instance, 16); + unknown.Chunk(3, 0, 0x2001); + unknown.Chunk(5, 0, 0); + bool unknownThrows = false; + try { + static_cast(StartBuffer(context, unknown.Data(), unknown.Bytes())); + } catch (const std::runtime_error&) { + unknownThrows = true; + } + Require(unknownThrows); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +// A batch whose headers carry the previous batch's bytes in the bits the console's builders never write +// (a PS4 trace of Knowledge is Power: Decades, whose Wwise reuses one batch buffer): bit 5 and bits 26, +// 27 and 31 of a job header, bits 4, 5 and 26 of a flags header, bits 5-31 of a buffer header and bits +// 4-31 of a return address header. It must run exactly as the clean batch does. +void TestConsoleStaleHeaderBits(std::uint32_t context) { + std::vector stream(1024, 0); + std::memcpy(stream.data(), AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME)); + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0); + std::int32_t result[2] = {-1, -1}; + std::vector pcm(1024, 0x55); + At9Run decoded{{-1, -1, 0, 0, 0}, 0, 0}; + const std::uint8_t initialize[8] = {0xFE, 0x70, 0x1F, 0xF0, 0, 0, 0, 0}; + + ConsoleBatch batch; + batch.Chunk(0x8C000030u, instance, 56); + batch.Buffer(0xFFFFFFF6u, &context, 0); + batch.Buffer(0xFFFFFFE2u, initialize, sizeof(initialize)); + batch.Chunk(0x04000033u, 0, 0x6001); + batch.Buffer(0xFFFFFFF2u, result, sizeof(result)); + batch.Chunk(0x8C000020u, instance, 72); + batch.Buffer(0xFFFFFFF6u, &context, 0); + batch.Buffer(0xFFFFFFE1u, stream.data(), stream.size()); + batch.Chunk(0x04000034u, 0x8000, 0x1001); + batch.Buffer(0xFFFFFFF1u, pcm.data(), pcm.size() * sizeof(std::int16_t)); + batch.Buffer(0xFFFFFFF2u, &decoded, sizeof(decoded)); + Require(StartBuffer(context, batch.Data(), batch.Bytes()) == 0); + Require(result[0] == 0 && result[1] == 0); + Require(decoded.stream.result == 0x18 && decoded.stream.inputConsumed == 1024 && decoded.stream.outputWritten == 1024 * 2 && decoded.frames == 4); + Require(std::all_of(pcm.begin(), pcm.end(), [](std::int16_t sample) { return sample == 0; })); Require(sceAjmInstanceDestroy(context, instance) == 0); } @@ -2088,6 +2301,10 @@ int main() { TestResampleAt9(context); TestAt9GaplessSegments(context); TestAt9RunDecodesWholeInput(context); + TestAt9FlagRules(context); TestPcmResample(context); + TestConsoleBufferJobs(context); + TestConsoleHandWrittenJobs(context); + TestConsoleStaleHeaderBits(context); Require(sceAjmFinalize(context) == 0); } diff --git a/core/libs/tests/GuestAjmVibration.cpp b/core/libs/tests/GuestAjmVibration.cpp index b130f5fbd2..8157596435 100644 --- a/core/libs/tests/GuestAjmVibration.cpp +++ b/core/libs/tests/GuestAjmVibration.cpp @@ -98,7 +98,7 @@ std::vector Decode(std::uint32_t context, unsigned channels, unsigned encodin Require((stream.result & 2) != 0 && stream.consumed == 0 && stream.produced == 0, "invalid first-frame flag"); } const auto stream = run(packet, packet.size()); - Require(stream.result == 0 && stream.internalResult == 0, "decode vibration superframe"); + Require(stream.result == 0x18 && stream.internalResult == 0, "decode vibration superframe"); Require(stream.consumed == 128 && stream.produced == static_cast(pcm.size() * sizeof(T)), "consumed/produced accounting"); Require(stream.samples == static_cast(samples) && stream.frames == 4, "decoded sample/frame accounting"); Require(sceAjmInstanceDestroy(context, instance) == 0, "destroy instance"); diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index 8c0a9695c5..b4206370e0 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -6,7 +6,7 @@ - [sceAjmInstanceExtend, sceAjmInstanceSwitch](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjm.native) - their four-argument signatures come only from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ajm.cs#L98-L104), whose comments do not describe them; shadPS4 [declares both without parameters](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ajm/ajm.h#L222-L223). Neither is measured on a console; these throwing declarations only resolve imports. - [sceAjmInstanceCreate](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a full 14-bit instance slot namespace returns `0x80930007` (`OUT_OF_RESOURCES`), as [shadPS4's InstanceCreate](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ajm/ajm_context.cpp) does for its own instance pool (PS4). The 16384 slots per codec prefix are this runtime's existing ID layout, not a measured console capacity; the PS5 capacity and exhaustion result are unverified. - [ATRAC9 vibration decoding](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): channel configurations 6 (mono) and 7 (dual mono) use the pinned [vgmstream decoder](https://github.com/vgmstream/vgmstream/blob/7c520e8f9b078dfde2f84f1b5bcbad0c6c415806/src/coding/libs/atrac9_dec.c), including its 12/24/48 kHz profile limits. Synthetic fixtures cover interleaving, reset after malformed input, partial superframes and PCM16/PCM32/float output; console PCM rounding and physical haptic output are unverified. Ordinary channel configurations and discrete multichannel streams still use LibAtrac9. Decoder creation and decoding run under the existing AJM instance lock because the dependency rebuilds shared Huffman tables during initialization. -- [sceAjmBatchJobGetInfo](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - writes the result and format sideband of a run job without buffers with the `SIDEBAND_FORMAT` flag (0x400000000000), as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does, not confirmed on a PS5 title; the format reports bitrate 0 for ATRAC9 as in KytyPS5, while [shadPS4](https://github.com/shadps4-emu/shadPS4) derives it from the superframe size; before the instance is initialized it returns the not-initialized result (1) and no format, like the other run jobs here, while KytyPS5 returns result 0 with the format of the uninitialized decoder +- [sceAjmBatchJobGetInfo](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - writes the result and format sideband of a run job without buffers with the `SIDEBAND_FORMAT` flag (0x400000000000), as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does, not confirmed on a PS5 title; the ATRAC9 format's bitrate is the superframe's bits per second (superframe bytes * 8 * sample rate / samples per superframe), as [shadPS4](https://github.com/shadps4-emu/shadPS4) derives it and as a PS4 reported it (168000 for 448-byte superframes at 48 kHz), where KytyPS5 reports 0; before the instance is initialized it returns the not-initialized result (1) and no format, like the other run jobs here, while KytyPS5 returns result 0 with the format of the uninitialized decoder - [sceAjmDecMp3ParseFrame](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: Layer I/II headers are accepted when no original-file-length (`parse_ofl`) lookup is requested, which is supported only for Layer III; MPEG-2.5 stops at 64 kbps. The `parse_ofl` layout (Xing/Info + LAME tag after side information, with the standard fixed Xing/Info offset retained for encoders such as [LAME](https://github.com/lameproject/lame/blob/master/libmp3lame/VbrTag.c#L854-L862), VBRI at byte 36 or after side information, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description. - [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title (the one call in PPSA04354 passes a 24-byte result buffer, the 8-byte result plus the 16-byte ATRAC9 codec info written here; the call does not show the codec or the fields); a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts - [AJM Opus decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout and little-endian `u16` packet byte count come from The Smurfs Dreams (`2, 48000, 0`) and Lost Soul Aside (`4, 48000, 1`). The third word is interpreted as the mapping family, as in [KytyPS5's initialize structure](https://github.com/KytyPS5/KytyPS5/blob/984cfcebd141b211aa9e0a8cde63b0ebe652d07a/src/libs/ajm/opus_decoder.h#L11-L15). Family 1 uses the conventional stream counts, coupled counts and channel map from [libopus 1.5.2's surround layout](https://github.com/xiph/opus/blob/v1.5.2/src/opus_multistream_encoder.c#L47-L55), passed to FFmpeg through an [RFC 7845 section 5.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1) header with zero pre-skip and gain; gapless skipping remains controlled by AJM sideband parameters. FFmpeg returns PCM in its native speaker order. The implicit stream layout, PCM channel order and invalid family-0 channel count result are not verified on a console. NHL 27 (PPSA34063) passes `1, 48000, 255`. Family 255 with one channel is decoded as one mono stream, the same as family 0; this is assumed, since the stream count, coupled stream count and channel mapping that a family-255 Opus header carries ([RFC 7845 section 5.1.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1.1)) are not passed. Only 48000 Hz, mapping families 0 and 1, and family 255 with one channel are implemented; other rates and families, and family 255 with more channels, throw @@ -17,6 +17,10 @@ - [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - initialize parameters are 8 bytes for ATRAC9 and M4AAC, 12 for Opus and none for MP3, as one title passes them; other codecs and sizes, and an output other than the 8-byte result, throw - [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - SIDEBAND_GAPLESS_DECODE with RESET overwrites both counts as in [RPCSX](https://github.com/RPCSX/rpcsx); shadPS4 ignores a zero count when the other is nonzero. Without RESET they differ, so it throws - [sceAjmBatchJobSetResampleParameters, sceAjmBatchJobGetResampleInfo](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm) - the ratio is taken as decoded samples per output sample and num_samples as the decoded samples the resampler holds, both inferred from FMOD (Hades II); the console's interpolation is unknown, a 4-point Catmull-Rom is used. The flags argument is ignored, as in shadPS4 and Kyty +- [sceAjmBatchJobControlBufferRa, sceAjmBatchJobRunBufferRa, sceAjmBatchJobRunSplitBufferRa, sceAjmBatchStartBuffer](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - the batch layout (chunk kinds, 20-bit payload, flags bits 32-51 as the flags chunk's payload, the builders' chunk order) follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/854b291c63360ff24e590809aff54cf5ab2dd4c9/src/core/libraries/ajm/ajm_batch.cpp) and was confirmed byte for byte for 64-byte control and 80-byte run jobs by a PS4 trace of Knowledge is Power: Decades (CUSA12435, Wwise, 32 545 batches dumped by a GoldHEN plugin). The console's builders write only bits 0-3 (kind), bit 4 (a buffer chunk's output direction) and bits 6-25 (payload) of the header word and leave bits 5, 26, 27 and 31 as the previous batch left them, so the kind is read as 4 bits plus the direction bit, not as 6 bits +- [sceAjmBatchStartBuffer](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - the statistics instance 0x80000 and the inline chunk come from shadPS4 only: the traced title builds every batch with the builders and a return address, and no title was seen writing an inline chunk. Flags are kept as passed. Unknown chunk kinds, control flags other than RESET, INITIALIZE and SIDEBAND_GAPLESS_DECODE, control jobs with run buffers and run jobs with a control input throw; a size not a multiple of 8, an overrunning chunk or a job without flags return 0x80930011 after the jobs before it ran +- [ATRAC9 run results](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - probe runs of a title's ATRAC9 stream through the console's builders on a PS4 showed the run result naming the side that ran out: `PARTIAL_INPUT` (0x8) when the input ran out with room left, on a superframe boundary or not; `NOT_ENOUGH_ROOM` (0x10) when the room ran out with input left, after any number of frames, and with nothing consumed for a zero room; both (0x18) when the input ended just as the output filled, which is every Wwise run job. ATRAC9 run jobs with `RUN_MULTIPLE_FRAMES` follow those rules here, vibration streams included, which were not traced; resampled runs and the other codecs keep the earlier rule (a trailing partial frame is partial input, a run that produced nothing is out of room) +- [ATRAC9 run without RUN_MULTIPLE_FRAMES](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the same PS4 probe decoded exactly one frame and reported 0 for such a run; here it still decodes its whole input, as the PS5 title in the gapless entry needs, so a PS4 title that relies on one frame per run would get more. The console also rejected the tests' synthetic silent superframe after three frames (`CODEC_ERROR | FATAL`, internal result -577) where LibAtrac9 decodes four, so that vector proves job plumbing, not decoding; on the title's stream LibAtrac9 matched the console's PCM within 4 LSB, 83.5% of the samples exactly ### Functional From 6559a413b0cce2112af92a94c24e65fe5ec1edfb Mon Sep 17 00:00:00 2001 From: ntamas94 Date: Thu, 22 Oct 2026 11:38:41 +0200 Subject: [PATCH 408/428] fix(libSceAjm): keep the libSceAjmi stub and refuse null batch buffers as a PS4 does Review of the PS4 batch buffer change. The libSceAjmi stub of sceAjmBatchJobRunBufferRa stays, declared with the _nid_disambig1 suffix so that both libraries export the NID without defining one name. sceAjmBatchStartBuffer reads the whole batch before running any job, as probe batches on a PS4 showed: a buffer chunk of size zero is dropped whatever its address, and a null address with a nonzero size refuses the batch with INVALID_ADDRESS in the error record together with the job chunk, the chunk's offset in the job and the job's return address, with nothing of the batch run. Inputs and outputs share that rule. The console-measured result flags move from test comments to the debt entries. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libSceAjm.native/src/Ajm.cpp | 97 ++++++++++++++++------ core/libs/prx/libSceAjmi/Export.cpp | 5 ++ core/libs/tests/GuestAjm.cpp | 86 ++++++++++++++++--- docs/dev/debt/libSceAjm.native.md | 3 +- docs/dev/debt/libSceAjmi.md | 1 + 5 files changed, 154 insertions(+), 38 deletions(-) diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 41baae55e0..6860c6ca37 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -37,6 +37,7 @@ constexpr int SCE_AJM_ERROR_INVALID_BATCH = static_cast(0x80930004); constexpr int SCE_AJM_ERROR_INVALID_PARAMETER = static_cast(0x80930005); constexpr int SCE_AJM_ERROR_OUT_OF_RESOURCES = static_cast(0x80930007); constexpr int SCE_AJM_ERROR_MALFORMED_BATCH = static_cast(0x80930011); +constexpr int SCE_AJM_ERROR_INVALID_ADDRESS = static_cast(0x80930016); constexpr std::int32_t AJM_RESULT_NOT_INITIALIZED = 0x00000001; constexpr std::int32_t AJM_RESULT_INVALID_DATA = 0x00000002; @@ -1785,17 +1786,38 @@ void* EndConsoleJob(std::uint8_t* job, std::uint8_t* end, std::uint32_t instance return end; } -// Runs one console job as the job of this file's kind it describes. The return address is only kept by -// the console for error reports. Returns false for a job whose chunks overrun it or that has no flags. -bool RunConsoleJob(std::uint32_t instance, const std::uint8_t* at, const std::uint8_t* end) { - JobHeader job{}; - job.instance = instance; +struct ConsoleJob { + JobHeader header; + std::vector buffers; +}; + +// What sceAjmBatchStartBuffer reports in the AjmBatchError when it refuses a batch: the error, the job +// chunk, the offending chunk's offset in the job and the job's return address, as a PS4 fills them for a +// buffer chunk with a null address and a nonzero size (INVALID_ADDRESS, nothing of the batch run). The +// other refusals here report MALFORMED_BATCH the same way; they were not measured. +struct ConsoleRefusal { + std::int32_t code = SCE_AJM_ERROR_MALFORMED_BATCH; + const std::uint8_t* job = nullptr; + std::uint32_t offset = 0; + const void* returnAddress = nullptr; +}; + +// Reads one console job: the chunks between the job header at `job` and `end`. A buffer chunk of size +// zero is dropped whatever its address, as a PS4 does. Returns false with the refusal filled for a null +// address with a nonzero size, for chunks that overrun the job and for a job without flags. +bool ReadConsoleJob(std::uint32_t instance, const std::uint8_t* job, const std::uint8_t* end, ConsoleJob& out, ConsoleRefusal& refusal) { + out.header = JobHeader{}; + out.header.instance = instance; std::vector inputs; std::vector outputs; AjmBuffer controlInput{}; AjmBuffer controlOutput{}; ConsoleChunk flagsKind = ConsoleChunk::Job; + refusal = ConsoleRefusal{}; + refusal.job = job; + const std::uint8_t* at = job + 2 * sizeof(std::uint32_t); while (at < end) { + refusal.offset = static_cast(at - job); if (end - at < 8) return false; std::uint32_t words[2]; std::memcpy(words, at, sizeof(words)); @@ -1803,7 +1825,7 @@ bool RunConsoleJob(std::uint32_t instance, const std::uint8_t* at, const std::ui if (kind == ConsoleChunk::ControlFlags || kind == ConsoleChunk::RunFlags) { if (flagsKind != ConsoleChunk::Job) return false; flagsKind = kind; - job.flags = (std::uint64_t{ConsoleChunkPayload(words[0])} << 32) | words[1]; + out.header.flags = (std::uint64_t{ConsoleChunkPayload(words[0])} << 32) | words[1]; at += sizeof(words); continue; } @@ -1814,34 +1836,44 @@ bool RunConsoleJob(std::uint32_t instance, const std::uint8_t* at, const std::ui AjmBuffer buffer{nullptr, words[1]}; std::memcpy(&buffer.ptr, at + sizeof(words), sizeof(buffer.ptr)); at += 16; - if (kind == ConsoleChunk::InputRunBuffer && buffer.size) inputs.push_back(buffer); - if (kind == ConsoleChunk::OutputRunBuffer && buffer.ptr && buffer.size) outputs.push_back(buffer); + if (kind == ConsoleChunk::ReturnAddress) { + refusal.returnAddress = buffer.ptr; + continue; + } + if (buffer.size == 0) continue; + if (!buffer.ptr) { + refusal.code = SCE_AJM_ERROR_INVALID_ADDRESS; + return false; + } + if (kind == ConsoleChunk::InputRunBuffer) inputs.push_back(buffer); + if (kind == ConsoleChunk::OutputRunBuffer) outputs.push_back(buffer); if (kind == ConsoleChunk::InputControlBuffer) controlInput = buffer; if (kind == ConsoleChunk::OutputControlBuffer) controlOutput = buffer; } + refusal.offset = static_cast(end - job); if (flagsKind == ConsoleChunk::Job) return false; - job.sideband = controlOutput.ptr; - job.sidebandSize = controlOutput.size; + JobHeader& header = out.header; + header.sideband = controlOutput.ptr; + header.sidebandSize = controlOutput.size; if (instance == CONSOLE_STATISTICS_INSTANCE) { - job.kind = JobKind::GetStatistics; + header.kind = JobKind::GetStatistics; } else if (flagsKind == ConsoleChunk::ControlFlags) { constexpr std::uint64_t implemented = CONSOLE_FLAGS_VERSION_AND_CODEC | CONTROL_RESET | CONTROL_INITIALIZE | SIDEBAND_GAPLESS_DECODE; - if ((job.flags & ~implemented) != 0) NotImplemented_nid_no_patch("sceAjmBatchStartBuffer (control job flags other than RESET, INITIALIZE and SIDEBAND_GAPLESS_DECODE)"); + if ((header.flags & ~implemented) != 0) NotImplemented_nid_no_patch("sceAjmBatchStartBuffer (control job flags other than RESET, INITIALIZE and SIDEBAND_GAPLESS_DECODE)"); if (!inputs.empty() || !outputs.empty()) NotImplemented_nid_no_patch("sceAjmBatchStartBuffer (control job with run buffers)"); - job.kind = JobKind::Control; + header.kind = JobKind::Control; if (controlInput.size) inputs.push_back(controlInput); } else { if (controlInput.size) NotImplemented_nid_no_patch("sceAjmBatchStartBuffer (run job with an input control buffer)"); - job.kind = JobKind::Run; + header.kind = JobKind::Run; } - job.inputCount = static_cast(inputs.size()); - job.outputCount = static_cast(outputs.size()); - AJM_TRACE("[ajm] console job: instance %u, %s flags 0x%llx, %u input and %u output buffers, control input %llu and output %llu bytes\n", instance, flagsKind == ConsoleChunk::ControlFlags ? "control" : "run", static_cast(job.flags), job.inputCount, job.outputCount, static_cast(controlInput.size), static_cast(controlOutput.size)); - inputs.insert(inputs.end(), outputs.begin(), outputs.end()); - Execute(job, inputs.data(), inputs.data() + job.inputCount); + header.inputCount = static_cast(inputs.size()); + header.outputCount = static_cast(outputs.size()); + AJM_TRACE("[ajm] console job: instance %u, %s flags 0x%llx, %u input and %u output buffers, control input %llu and output %llu bytes\n", instance, flagsKind == ConsoleChunk::ControlFlags ? "control" : "run", static_cast(header.flags), header.inputCount, header.outputCount, static_cast(controlInput.size), static_cast(controlOutput.size)); + out.buffers = std::move(inputs); + out.buffers.insert(out.buffers.end(), outputs.begin(), outputs.end()); return true; } - } extern "C" { @@ -2132,23 +2164,38 @@ int APS5_VABI sceAjmBatchStartBuffer(uint32_t context, const uint8_t* batch, uin if (context == 0 || context >= g_nextContext.load(std::memory_order_relaxed)) return SCE_AJM_ERROR_INVALID_CONTEXT; if (!batch_id || (!batch && batch_size)) return SCE_AJM_ERROR_INVALID_PARAMETER; AJM_TRACE("[ajm] batch start buffer: context %u, priority %d, %u bytes\n", context, priority, batch_size); - if (batch_size % 8 != 0) return SCE_AJM_ERROR_MALFORMED_BATCH; + std::vector jobs; + ConsoleRefusal refusal; const std::uint8_t* at = batch; const std::uint8_t* end = batch + batch_size; - while (at < end) { - if (end - at < 8) return SCE_AJM_ERROR_MALFORMED_BATCH; + bool accepted = batch_size % 8 == 0; + while (accepted && at < end) { + refusal = ConsoleRefusal{}; + refusal.job = at; + if (end - at < 8) { + accepted = false; + break; + } std::uint32_t words[2]; std::memcpy(words, at, sizeof(words)); const std::uint8_t* body = at + sizeof(words); - if (words[1] > static_cast(end - body)) return SCE_AJM_ERROR_MALFORMED_BATCH; + if (words[1] > static_cast(end - body)) { + accepted = false; + break; + } const auto kind = ConsoleChunkKind(words[0]); if (kind == ConsoleChunk::Job) { - if (!RunConsoleJob(ConsoleChunkPayload(words[0]), body, body + words[1])) return SCE_AJM_ERROR_MALFORMED_BATCH; + accepted = ReadConsoleJob(ConsoleChunkPayload(words[0]), at, body + words[1], jobs.emplace_back(), refusal); } else if (kind != ConsoleChunk::InlineBuffer) { NotImplemented_nid_no_patch(("sceAjmBatchStartBuffer (batch chunk kind " + std::to_string(static_cast(kind)) + ")").c_str()); } at = body + words[1]; } + if (!accepted) { + if (error) *error = AjmBatchError{refusal.code, refusal.job, refusal.offset, refusal.returnAddress}; + return SCE_AJM_ERROR_MALFORMED_BATCH; + } + for (const ConsoleJob& job : jobs) Execute(job.header, job.buffers.data(), job.buffers.data() + job.header.inputCount); return RegisterBatch(context, error, batch_id); } diff --git a/core/libs/prx/libSceAjmi/Export.cpp b/core/libs/prx/libSceAjmi/Export.cpp index 7932a6f3c8..bdb3e1159d 100644 --- a/core/libs/prx/libSceAjmi/Export.cpp +++ b/core/libs/prx/libSceAjmi/Export.cpp @@ -13,4 +13,9 @@ int APS5_VABI sceAjmBatchJobInlineBuffer(void) { return 0; } +int APS5_VABI sceAjmBatchJobRunBufferRa_nid_disambig1(void) { + NotImplemented_nid_no_patch("sceAjmBatchJobRunBufferRa"); + return 0; +} + } diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index f850b844ff..92312ea2d2 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -1051,7 +1051,6 @@ std::vector DecodeAt9Superframe(std::uint32_t context, const std:: Require(sceAjmBatchJobRun(&info, instance, sidebandStream | runMultipleFrames, superframe.data(), superframe.size(), pcm.data(), pcm.size() * sizeof(std::int16_t), &decoded, sizeof(decoded)) == 0); Submit(context, info); Require(initResult[0] == 0); - // one superframe into exactly its 1024 samples: PARTIAL_INPUT | NOT_ENOUGH_ROOM as on the console Require(decoded.stream.result == 0x18 && static_cast(decoded.stream.inputConsumed) == superframe.size()); Require(static_cast(decoded.stream.outputWritten) == pcm.size() * sizeof(std::int16_t) && decoded.stream.totalDecodedSamples == 1024 && decoded.frames == 4); Require(sceAjmInstanceDestroy(context, instance) == 0); @@ -1579,7 +1578,6 @@ ResampledStream Stream(std::uint32_t context, std::uint32_t instance, const std: Require(sceAjmBatchJobDecode(&info, instance, stream + offset, size - offset, pcm.data(), pcm.size() * sizeof(std::int16_t), &sideband) == 0); Submit(context, info); if (ratio > 0) Require(setResult[0] == 0); - // 0x8 / 0x10 / 0x18: which of input and room ran out (what the console reports, see TestAt9FlagRules) Require(sideband.result == 0 || sideband.result == 0x8 || sideband.result == 0x10 || sideband.result == 0x18 || (offset == size && sideband.outputWritten == 0)); offset += static_cast(sideband.inputConsumed); if (offset < size && static_cast(sideband.outputWritten) < pcm.size() * sizeof(std::int16_t)) ++out.shortJobs; @@ -1800,7 +1798,7 @@ At9Run RunAt9Job(std::uint32_t context, std::uint32_t instance, std::uint64_t fl const std::size_t sidebandSize = (flags & runMultipleFrames) != 0 ? sizeof(decoded) : sizeof(decoded.stream); Require(sceAjmBatchJobRun(&info, instance, sidebandStream | flags, input, inputSize, pcm.data(), pcm.size() * sizeof(std::int16_t), &decoded, sidebandSize) == 0); Submit(context, info); - Require(decoded.stream.result == 0 || decoded.stream.result == 0x8 || decoded.stream.result == 0x18); // which side ran out, see TestAt9FlagRules + Require(decoded.stream.result == 0 || decoded.stream.result == 0x8 || decoded.stream.result == 0x18); return decoded; } @@ -1834,14 +1832,10 @@ void TestAt9RunDecodesWholeInput(std::uint32_t context) { RunControl(context, instance, CONTROL_START, &start, sizeof(start), result); Require(result[0] == 0); const auto run = RunAt9Job(context, instance, 0, stream.data(), stream.size()); - // 1024 bytes decode into exactly the 1024 samples of room: the console reports PARTIAL_INPUT | NOT_ENOUGH_ROOM Require(run.stream.result == 0x18 && static_cast(run.stream.inputConsumed) == stream.size() && run.stream.outputWritten == 1024 * 2); Require(sceAjmInstanceDestroy(context, instance) == 0); } -// The result flags say which side ran out, as a PS4 reports them for a title's ATRAC9 stream: the input -// (PARTIAL_INPUT, even on a superframe boundary) with room left, the room (NOT_ENOUGH_ROOM) with input -// left after some frames or with a zero room before any, or both at once (0x18). void TestAt9FlagRules(std::uint32_t context) { constexpr std::uint64_t runMultipleFrames = 1ull << 12; constexpr std::uint64_t sidebandStream = 1ull << 47; @@ -1925,7 +1919,6 @@ void TestConsoleBufferJobs(std::uint32_t context) { Require(StartBuffer(context, batch.data(), static_cast(at) - batch.data()) == 0); Require(result[0] == 0 && result[1] == 0); - // input and output both end with the fourth frame: the console reports PARTIAL_INPUT | NOT_ENOUGH_ROOM constexpr std::int32_t exactFit = 0x18; Require(single.stream.result == exactFit && single.stream.inputConsumed == 1024 && single.stream.outputWritten == 1024 * 2 && single.frames == 4 && single.stream.totalDecodedSamples == 1024); Require(split.stream.result == exactFit && split.stream.inputConsumed == 1024 && split.stream.outputWritten == 1024 * 2 && split.frames == 4 && split.stream.totalDecodedSamples == 2048); @@ -2016,10 +2009,78 @@ void TestConsoleHandWrittenJobs(std::uint32_t context) { Require(sceAjmInstanceDestroy(context, instance) == 0); } -// A batch whose headers carry the previous batch's bytes in the bits the console's builders never write -// (a PS4 trace of Knowledge is Power: Decades, whose Wwise reuses one batch buffer): bit 5 and bits 26, -// 27 and 31 of a job header, bits 4, 5 and 26 of a flags header, bits 5-31 of a buffer header and bits -// 4-31 of a return address header. It must run exactly as the clean batch does. +int StartBufferRefused(std::uint32_t context, const void* batch, std::size_t size, AjmBatchError& error) { + std::uint32_t id = 0; + error = AjmBatchError{}; + return sceAjmBatchStartBuffer(context, static_cast(batch), static_cast(size), 0, &error, &id); +} + +void TestConsoleBufferEdges(std::uint32_t context) { + constexpr int malformedBatch = static_cast(0x80930011); + constexpr int invalidAddress = static_cast(0x80930016); + std::vector stream(1024, 0); + std::memcpy(stream.data(), AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME)); + const std::uint8_t initialize[8] = {0xFE, 0x70, 0x1F, 0xF0, 0, 0, 0, 0}; + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0); + std::vector pcm(1024, 0x55); + std::int32_t result[2] = {-1, -1}; + const auto control = [&](ConsoleBatch& batch) { + result[0] = result[1] = -1; + batch.Chunk(0, instance, 40); + batch.Buffer(2, initialize, sizeof(initialize)); + batch.Chunk(3, 0, 0x6001); + batch.Buffer(18, result, sizeof(result)); + }; + + At9Run dropped{{-1, -1, 0, 0, 0}, 0, 0}; + ConsoleBatch zeroSizes; + control(zeroSizes); + zeroSizes.Chunk(0, instance, 104); + zeroSizes.Buffer(1, stream.data(), 0); + zeroSizes.Buffer(1, stream.data(), stream.size()); + zeroSizes.Chunk(4, 0x8000, 0x1001); + zeroSizes.Buffer(17, pcm.data(), 0); + zeroSizes.Buffer(17, pcm.data(), pcm.size() * sizeof(std::int16_t)); + zeroSizes.Buffer(17, nullptr, 0); + zeroSizes.Buffer(18, &dropped, sizeof(dropped)); + Require(StartBuffer(context, zeroSizes.Data(), zeroSizes.Bytes()) == 0); + Require(result[0] == 0 && dropped.stream.result == 0x18 && dropped.stream.inputConsumed == 1024 && dropped.stream.outputWritten == 1024 * 2 && dropped.frames == 4); + Require(std::all_of(pcm.begin(), pcm.end(), [](std::int16_t sample) { return sample == 0; })); + + At9Run noRoom{{-1, -1, 0, 0, 0}, 0, 0}; + std::fill(pcm.begin(), pcm.end(), std::int16_t{0x55}); + ConsoleBatch zeroRoom; + control(zeroRoom); + zeroRoom.Chunk(0, instance, 56); + zeroRoom.Buffer(1, stream.data(), stream.size()); + zeroRoom.Chunk(4, 0x8000, 0x1001); + zeroRoom.Buffer(17, pcm.data(), 0); + zeroRoom.Buffer(18, &noRoom, sizeof(noRoom)); + Require(StartBuffer(context, zeroRoom.Data(), zeroRoom.Bytes()) == 0); + Require(result[0] == 0 && noRoom.stream.result == 0x10 && noRoom.stream.inputConsumed == 0 && noRoom.stream.outputWritten == 0 && noRoom.frames == 0); + Require(std::all_of(pcm.begin(), pcm.end(), [](std::int16_t sample) { return sample == 0x55; })); + + const void* returnAddress = &stream; + for (const bool nullInput : {true, false}) { + At9Run untouched{{-1, -1, 0, 0, 0}, 0, 0}; + ConsoleBatch refused; + control(refused); + refused.Chunk(0, instance, 72); + refused.Buffer(6, returnAddress, 0); + refused.Buffer(1, nullInput ? nullptr : stream.data(), stream.size()); + refused.Chunk(4, 0x8000, 0x1001); + refused.Buffer(17, nullInput ? pcm.data() : nullptr, pcm.size() * sizeof(std::int16_t)); + refused.Buffer(18, &untouched, sizeof(untouched)); + AjmBatchError error{}; + Require(StartBufferRefused(context, refused.Data(), refused.Bytes(), error) == malformedBatch); + Require(error.error_code == invalidAddress && error.job_addr == reinterpret_cast(refused.Data()) + 48); + Require(error.cmd_offset == (nullInput ? 24u : 48u) && error.job_ra == returnAddress); + Require(result[0] == -1 && untouched.stream.result == -1); + } + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + void TestConsoleStaleHeaderBits(std::uint32_t context) { std::vector stream(1024, 0); std::memcpy(stream.data(), AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME)); @@ -2305,6 +2366,7 @@ int main() { TestPcmResample(context); TestConsoleBufferJobs(context); TestConsoleHandWrittenJobs(context); + TestConsoleBufferEdges(context); TestConsoleStaleHeaderBits(context); Require(sceAjmFinalize(context) == 0); } diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index b4206370e0..7e299c624a 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -18,7 +18,8 @@ - [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - SIDEBAND_GAPLESS_DECODE with RESET overwrites both counts as in [RPCSX](https://github.com/RPCSX/rpcsx); shadPS4 ignores a zero count when the other is nonzero. Without RESET they differ, so it throws - [sceAjmBatchJobSetResampleParameters, sceAjmBatchJobGetResampleInfo](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm) - the ratio is taken as decoded samples per output sample and num_samples as the decoded samples the resampler holds, both inferred from FMOD (Hades II); the console's interpolation is unknown, a 4-point Catmull-Rom is used. The flags argument is ignored, as in shadPS4 and Kyty - [sceAjmBatchJobControlBufferRa, sceAjmBatchJobRunBufferRa, sceAjmBatchJobRunSplitBufferRa, sceAjmBatchStartBuffer](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - the batch layout (chunk kinds, 20-bit payload, flags bits 32-51 as the flags chunk's payload, the builders' chunk order) follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/854b291c63360ff24e590809aff54cf5ab2dd4c9/src/core/libraries/ajm/ajm_batch.cpp) and was confirmed byte for byte for 64-byte control and 80-byte run jobs by a PS4 trace of Knowledge is Power: Decades (CUSA12435, Wwise, 32 545 batches dumped by a GoldHEN plugin). The console's builders write only bits 0-3 (kind), bit 4 (a buffer chunk's output direction) and bits 6-25 (payload) of the header word and leave bits 5, 26, 27 and 31 as the previous batch left them, so the kind is read as 4 bits plus the direction bit, not as 6 bits -- [sceAjmBatchStartBuffer](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - the statistics instance 0x80000 and the inline chunk come from shadPS4 only: the traced title builds every batch with the builders and a return address, and no title was seen writing an inline chunk. Flags are kept as passed. Unknown chunk kinds, control flags other than RESET, INITIALIZE and SIDEBAND_GAPLESS_DECODE, control jobs with run buffers and run jobs with a control input throw; a size not a multiple of 8, an overrunning chunk or a job without flags return 0x80930011 after the jobs before it ran +- [sceAjmBatchStartBuffer](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - the statistics instance 0x80000 and the inline chunk come from shadPS4 only: the traced title builds every batch with the builders and a return address, and no title was seen writing an inline chunk. Flags are kept as passed. Unknown chunk kinds, control flags other than RESET, INITIALIZE and SIDEBAND_GAPLESS_DECODE, control jobs with run buffers and run jobs with a control input throw +- [sceAjmBatchStartBuffer refusals](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - probe batches through the console's builders on a PS4 showed that a run buffer chunk of size zero is dropped whatever its address (the job runs as without it: 0x10 and nothing consumed for no room, 0 for no input), and that a null address with a nonzero size refuses the batch before any of its jobs runs, with 0x80930011 returned and the error record holding 0x80930016, the job chunk's address, the chunk's offset in the job and the job's return address. Control buffer chunks follow the same rule here, not measured. A size not a multiple of 8, a chunk that overruns its job or the batch, or a job without flags are refused the same way with 0x80930011 in the record; the console's report for those was not measured - [ATRAC9 run results](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - probe runs of a title's ATRAC9 stream through the console's builders on a PS4 showed the run result naming the side that ran out: `PARTIAL_INPUT` (0x8) when the input ran out with room left, on a superframe boundary or not; `NOT_ENOUGH_ROOM` (0x10) when the room ran out with input left, after any number of frames, and with nothing consumed for a zero room; both (0x18) when the input ended just as the output filled, which is every Wwise run job. ATRAC9 run jobs with `RUN_MULTIPLE_FRAMES` follow those rules here, vibration streams included, which were not traced; resampled runs and the other codecs keep the earlier rule (a trailing partial frame is partial input, a run that produced nothing is out of room) - [ATRAC9 run without RUN_MULTIPLE_FRAMES](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the same PS4 probe decoded exactly one frame and reported 0 for such a run; here it still decodes its whole input, as the PS5 title in the gapless entry needs, so a PS4 title that relies on one frame per run would get more. The console also rejected the tests' synthetic silent superframe after three frames (`CODEC_ERROR | FATAL`, internal result -577) where LibAtrac9 decodes four, so that vector proves job plumbing, not decoding; on the title's stream LibAtrac9 matched the console's PCM within 4 LSB, 83.5% of the samples exactly diff --git a/docs/dev/debt/libSceAjmi.md b/docs/dev/debt/libSceAjmi.md index d3ac224c47..a1e8ac5685 100644 --- a/docs/dev/debt/libSceAjmi.md +++ b/docs/dev/debt/libSceAjmi.md @@ -3,3 +3,4 @@ ### Unknown function info - [libSceAjmi pending exports](../../../core/libs/prx/libSceAjmi/Export.cpp) - 5 named callable exports were identified in the PS5 3.20 export table and cross-checked against firmware 13.60. Signatures and behavior remain unverified; declarations use the unknown-signature convention and throw through `NotImplemented_nid_no_patch`. Evidence: [3.20 export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAjmi.c). +- [sceAjmBatchJobRunBufferRa](../../../core/libs/prx/libSceAjmi/Export.cpp) (libSceAjmi) - declared as `sceAjmBatchJobRunBufferRa_nid_disambig1`, which the NID patcher exports under the name's NID, because libSceAjm implements the same name for PS4 titles; a title importing it from libSceAjmi still gets the not-implemented error at call time From e3917fa218d9e63bd6807647e9b025c2e1af1a3a Mon Sep 17 00:00:00 2001 From: ntamas94 Date: Thu, 22 Oct 2026 11:40:40 +0200 Subject: [PATCH 409/428] docs(libSceAjm): align the gapless and libSceAjmi debt entries with the PS4 probe The gapless entry no longer says the one-frame behaviour is unverified, since the probe measured it, and the libSceAjmi entry names the two exports its file declares. Co-Authored-By: Claude Fable 5.1 --- docs/dev/debt/libSceAjm.native.md | 4 ++-- docs/dev/debt/libSceAjmi.md | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index 7e299c624a..944c0c111c 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -21,7 +21,7 @@ - [sceAjmBatchStartBuffer](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - the statistics instance 0x80000 and the inline chunk come from shadPS4 only: the traced title builds every batch with the builders and a return address, and no title was seen writing an inline chunk. Flags are kept as passed. Unknown chunk kinds, control flags other than RESET, INITIALIZE and SIDEBAND_GAPLESS_DECODE, control jobs with run buffers and run jobs with a control input throw - [sceAjmBatchStartBuffer refusals](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - probe batches through the console's builders on a PS4 showed that a run buffer chunk of size zero is dropped whatever its address (the job runs as without it: 0x10 and nothing consumed for no room, 0 for no input), and that a null address with a nonzero size refuses the batch before any of its jobs runs, with 0x80930011 returned and the error record holding 0x80930016, the job chunk's address, the chunk's offset in the job and the job's return address. Control buffer chunks follow the same rule here, not measured. A size not a multiple of 8, a chunk that overruns its job or the batch, or a job without flags are refused the same way with 0x80930011 in the record; the console's report for those was not measured - [ATRAC9 run results](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - probe runs of a title's ATRAC9 stream through the console's builders on a PS4 showed the run result naming the side that ran out: `PARTIAL_INPUT` (0x8) when the input ran out with room left, on a superframe boundary or not; `NOT_ENOUGH_ROOM` (0x10) when the room ran out with input left, after any number of frames, and with nothing consumed for a zero room; both (0x18) when the input ended just as the output filled, which is every Wwise run job. ATRAC9 run jobs with `RUN_MULTIPLE_FRAMES` follow those rules here, vibration streams included, which were not traced; resampled runs and the other codecs keep the earlier rule (a trailing partial frame is partial input, a run that produced nothing is out of room) -- [ATRAC9 run without RUN_MULTIPLE_FRAMES](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the same PS4 probe decoded exactly one frame and reported 0 for such a run; here it still decodes its whole input, as the PS5 title in the gapless entry needs, so a PS4 title that relies on one frame per run would get more. The console also rejected the tests' synthetic silent superframe after three frames (`CODEC_ERROR | FATAL`, internal result -577) where LibAtrac9 decodes four, so that vector proves job plumbing, not decoding; on the title's stream LibAtrac9 matched the console's PCM within 4 LSB, 83.5% of the samples exactly +- [ATRAC9 run without RUN_MULTIPLE_FRAMES](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the same PS4 probe decoded exactly one frame and reported 0 for such a run; here it still decodes its whole input, as FMOD in PPSA29141 needs (it puts two run jobs in one batch with the second's input right after the whole of the first's), so a PS4 title that relies on one frame per run would get more; MP3 and Opus run jobs still stop after one frame. The console also rejected the tests' synthetic silent superframe after three frames (`CODEC_ERROR | FATAL`, internal result -577) where LibAtrac9 decodes four, so that vector proves job plumbing, not decoding; on the title's stream LibAtrac9 matched the console's PCM within 4 LSB, 83.5% of the samples exactly ### Functional @@ -29,7 +29,7 @@ - [AJM batch contexts](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): start, wait and cancel reject context IDs not allocated by initialization; batches belong to their creating context, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0ebea15c24253b60f514fc37a348dabc8d0bcb90/src/core/libraries/ajm/ajm.cpp). This is not verified on PS5. Finalization still does not invalidate contexts or release their instances and outstanding batches; jobs execute synchronously and cancellation does not interrupt them. - [sceAjmBatchStart](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp): console results for malformed batch descriptors and corrupted jobs are unverified. A null batch buffer or an offset beyond its capacity returns the existing invalid-parameter error; a truncated or inconsistent private packed job throws before its payload is read. The packed representation belongs to this implementation. -- [ATRAC9 gapless segments](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a run job stops after the gapless total of output samples counted from the start of the segment, leaving the rest of its input unconsumed, and the next run starts the next segment with the decoder as it is and without a second skip. FMOD in PPSA29141 sets a total shorter than a sound's frames and reads the frames after it on later runs; with the output held at the total its stream reader waited for them forever and the last buffer repeated. The console's handling of the frames after the total is not documented; a reset (`SetGaplessDecode` with the reset flag, `ClearContext`, initialization) starts a new segment. An ATRAC9 run job without `RUN_MULTIPLE_FRAMES` decodes its whole input as one with it: FMOD in PPSA29141 puts two run jobs in one batch with the second's input right after the whole of the first's. Whether the console stops after one frame there is not verified; MP3 and Opus run jobs still stop after one frame +- [ATRAC9 gapless segments](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a run job stops after the gapless total of output samples counted from the start of the segment, leaving the rest of its input unconsumed, and the next run starts the next segment with the decoder as it is and without a second skip. FMOD in PPSA29141 sets a total shorter than a sound's frames and reads the frames after it on later runs; with the output held at the total its stream reader waited for them forever and the last buffer repeated. The console's handling of the frames after the total is not documented; a reset (`SetGaplessDecode` with the reset flag, `ClearContext`, initialization) starts a new segment. - [AJM M4AAC decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 2) - Ghost of Yotei (PPSA26344) initializes it through `sceAjmBatchJobControl`. The initialize parameters (`u32` config type, 1 ADTS or 2 raw, then `u32` sampling frequency index), instance flags (channels in bits 3-6, `ENABLE_SBR_DECODE` bit 32, `ENABLE_NONDELAY_OUTPUT` bit 33, surround interleave order bits 34-35), `{heaac, reserved}` codec info and decoder error result (`CODEC_ERROR | FATAL`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/ajm/ajm_aac.cpp) (PS4, fdk-aac) and are unverified on PS5; decoding uses FFmpeg's AAC decoder. Without `ENABLE_NONDELAY_OUTPUT`, the first two frames after initialization or reset are consumed without output. Raw input takes its channel configuration from the instance (0 means stereo) and is decoded as one access unit per run job. The format reports the last frame's bitrate. Invalid config type or raw sampling frequency index above 11 returns invalid-parameter result 4. The implementation throws for ADTS without a frame header, profiles other than AAC LC, multiple raw data blocks per ADTS frame, unsupported channel configurations, more channels than the instance supports, HE-AAC without `ENABLE_SBR_DECODE`, raw input with `RUN_MULTIPLE_FRAMES`, output room smaller than the previous frame length, and unknown codec flags. - [AJM](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp), [APR](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) and [NGS2](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) state is allocated with `new` and never freed on purpose: title threads still call into these libraries while `std::exit` runs static destructors, so ordinary statics would be destroyed under them. Keep them leaked; `guest_ajm_teardown` covers the AJM case on Unix. - [AJM Opus mapping family 255 with several channels](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): the initialization words carry no stream count or channel mapping, so the decoder opens on the first packet. It counts the self-delimited streams of that multistream packet ([RFC 6716 appendix B](https://datatracker.ietf.org/doc/html/rfc6716#appendix-B)) and takes each stream's stereo flag from its TOC byte as coupled, then decodes with an identity channel mapping. A packet whose streams do not add up to the channel count throws. The layout is inferred, not read from the console; unverified on hardware. diff --git a/docs/dev/debt/libSceAjmi.md b/docs/dev/debt/libSceAjmi.md index a1e8ac5685..0a5d293464 100644 --- a/docs/dev/debt/libSceAjmi.md +++ b/docs/dev/debt/libSceAjmi.md @@ -2,5 +2,4 @@ ### Unknown function info -- [libSceAjmi pending exports](../../../core/libs/prx/libSceAjmi/Export.cpp) - 5 named callable exports were identified in the PS5 3.20 export table and cross-checked against firmware 13.60. Signatures and behavior remain unverified; declarations use the unknown-signature convention and throw through `NotImplemented_nid_no_patch`. Evidence: [3.20 export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAjmi.c). -- [sceAjmBatchJobRunBufferRa](../../../core/libs/prx/libSceAjmi/Export.cpp) (libSceAjmi) - declared as `sceAjmBatchJobRunBufferRa_nid_disambig1`, which the NID patcher exports under the name's NID, because libSceAjm implements the same name for PS4 titles; a title importing it from libSceAjmi still gets the not-implemented error at call time +- [libSceAjmi pending exports](../../../core/libs/prx/libSceAjmi/Export.cpp) - of the 5 named callable exports identified in the PS5 3.20 export table and cross-checked against firmware 13.60, 2 are declared here: `sceAjmBatchJobInlineBuffer` and `sceAjmBatchJobRunBufferRa`, the latter as `sceAjmBatchJobRunBufferRa_nid_disambig1`, which the NID patcher exports under the name's NID, because libSceAjm implements the same name for PS4 titles; a title importing it from libSceAjmi still gets the not-implemented error at call time. Signatures and behavior remain unverified; the declarations use the unknown-signature convention and throw through `NotImplemented_nid_no_patch`. Evidence: [3.20 export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAjmi.c). From 46f0e90b925430dea558c4b8c0dd545700ad270e Mon Sep 17 00:00:00 2001 From: ntamas94 Date: Thu, 22 Oct 2026 15:32:59 +0200 Subject: [PATCH 410/428] fix(libSceAjm): reject a null PS4 batch address sceAjmBatchStartBuffer let a null batch of zero bytes through its parameter check and formed the end pointer from it. A null batch now returns INVALID_PARAMETER whatever the size, as the PS5 builders' sceAjmBatchInitialize does; a batch of zero bytes still starts, runs nothing and completes at once. A test covers both; neither was measured on a console, which the debt entry says. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libSceAjm.native/src/Ajm.cpp | 2 +- core/libs/tests/GuestAjm.cpp | 16 ++++++++++++++++ docs/dev/debt/libSceAjm.native.md | 1 + 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 6860c6ca37..deb893d345 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -2162,7 +2162,7 @@ void* APS5_VABI sceAjmBatchJobRunBufferRa(void* position, uint32_t instance, uin int APS5_VABI sceAjmBatchStartBuffer(uint32_t context, const uint8_t* batch, uint32_t batch_size, int priority, AjmBatchError* error, uint32_t* batch_id) { if (context == 0 || context >= g_nextContext.load(std::memory_order_relaxed)) return SCE_AJM_ERROR_INVALID_CONTEXT; - if (!batch_id || (!batch && batch_size)) return SCE_AJM_ERROR_INVALID_PARAMETER; + if (!batch_id || !batch) return SCE_AJM_ERROR_INVALID_PARAMETER; AJM_TRACE("[ajm] batch start buffer: context %u, priority %d, %u bytes\n", context, priority, batch_size); std::vector jobs; ConsoleRefusal refusal; diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 92312ea2d2..894715ca9a 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -2081,6 +2081,21 @@ void TestConsoleBufferEdges(std::uint32_t context) { Require(sceAjmInstanceDestroy(context, instance) == 0); } +void TestConsoleEmptyBatch(std::uint32_t context) { + constexpr int invalidParameter = static_cast(0x80930005); + constexpr int invalidBatch = static_cast(0x80930004); + std::uint32_t id = 0; + AjmBatchError error{}; + Require(sceAjmBatchStartBuffer(context, nullptr, 0, 0, &error, &id) == invalidParameter); + Require(sceAjmBatchStartBuffer(context, nullptr, 16, 0, &error, &id) == invalidParameter); + const std::uint64_t words[2] = {0, 0}; + Require(sceAjmBatchStartBuffer(context, reinterpret_cast(words), 0, 0, &error, nullptr) == invalidParameter); + error.error_code = -1; + Require(sceAjmBatchStartBuffer(context, reinterpret_cast(words), 0, 0, &error, &id) == 0); + Require(error.error_code == 0 && id != 0 && sceAjmBatchWait(context, id, 0, &error) == 0); + Require(sceAjmBatchWait(context, id, 0, &error) == invalidBatch); +} + void TestConsoleStaleHeaderBits(std::uint32_t context) { std::vector stream(1024, 0); std::memcpy(stream.data(), AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME)); @@ -2367,6 +2382,7 @@ int main() { TestConsoleBufferJobs(context); TestConsoleHandWrittenJobs(context); TestConsoleBufferEdges(context); + TestConsoleEmptyBatch(context); TestConsoleStaleHeaderBits(context); Require(sceAjmFinalize(context) == 0); } diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index 944c0c111c..d37af30120 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -20,6 +20,7 @@ - [sceAjmBatchJobControlBufferRa, sceAjmBatchJobRunBufferRa, sceAjmBatchJobRunSplitBufferRa, sceAjmBatchStartBuffer](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - the batch layout (chunk kinds, 20-bit payload, flags bits 32-51 as the flags chunk's payload, the builders' chunk order) follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/854b291c63360ff24e590809aff54cf5ab2dd4c9/src/core/libraries/ajm/ajm_batch.cpp) and was confirmed byte for byte for 64-byte control and 80-byte run jobs by a PS4 trace of Knowledge is Power: Decades (CUSA12435, Wwise, 32 545 batches dumped by a GoldHEN plugin). The console's builders write only bits 0-3 (kind), bit 4 (a buffer chunk's output direction) and bits 6-25 (payload) of the header word and leave bits 5, 26, 27 and 31 as the previous batch left them, so the kind is read as 4 bits plus the direction bit, not as 6 bits - [sceAjmBatchStartBuffer](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - the statistics instance 0x80000 and the inline chunk come from shadPS4 only: the traced title builds every batch with the builders and a return address, and no title was seen writing an inline chunk. Flags are kept as passed. Unknown chunk kinds, control flags other than RESET, INITIALIZE and SIDEBAND_GAPLESS_DECODE, control jobs with run buffers and run jobs with a control input throw - [sceAjmBatchStartBuffer refusals](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - probe batches through the console's builders on a PS4 showed that a run buffer chunk of size zero is dropped whatever its address (the job runs as without it: 0x10 and nothing consumed for no room, 0 for no input), and that a null address with a nonzero size refuses the batch before any of its jobs runs, with 0x80930011 returned and the error record holding 0x80930016, the job chunk's address, the chunk's offset in the job and the job's return address. Control buffer chunks follow the same rule here, not measured. A size not a multiple of 8, a chunk that overruns its job or the batch, or a job without flags are refused the same way with 0x80930011 in the record; the console's report for those was not measured +- [sceAjmBatchStartBuffer null and empty batches](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - a null batch address returns 0x80930005 whatever the size, as the PS5 builders' sceAjmBatchInitialize does for a null buffer, and a batch of zero bytes is accepted, runs nothing and completes at once; neither was measured on a console (the probe that tried a null batch hung the probe app before writing a result) - [ATRAC9 run results](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - probe runs of a title's ATRAC9 stream through the console's builders on a PS4 showed the run result naming the side that ran out: `PARTIAL_INPUT` (0x8) when the input ran out with room left, on a superframe boundary or not; `NOT_ENOUGH_ROOM` (0x10) when the room ran out with input left, after any number of frames, and with nothing consumed for a zero room; both (0x18) when the input ended just as the output filled, which is every Wwise run job. ATRAC9 run jobs with `RUN_MULTIPLE_FRAMES` follow those rules here, vibration streams included, which were not traced; resampled runs and the other codecs keep the earlier rule (a trailing partial frame is partial input, a run that produced nothing is out of room) - [ATRAC9 run without RUN_MULTIPLE_FRAMES](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the same PS4 probe decoded exactly one frame and reported 0 for such a run; here it still decodes its whole input, as FMOD in PPSA29141 needs (it puts two run jobs in one batch with the second's input right after the whole of the first's), so a PS4 title that relies on one frame per run would get more; MP3 and Opus run jobs still stop after one frame. The console also rejected the tests' synthetic silent superframe after three frames (`CODEC_ERROR | FATAL`, internal result -577) where LibAtrac9 decodes four, so that vector proves job plumbing, not decoding; on the title's stream LibAtrac9 matched the console's PCM within 4 LSB, 83.5% of the samples exactly From 414985e0736550dfd8adcb69155ecf724ba9b544 Mon Sep 17 00:00:00 2001 From: ntamas94 Date: Thu, 22 Oct 2026 15:38:22 +0200 Subject: [PATCH 411/428] docs(libSceAjm): say the null batch probe froze the console Co-Authored-By: Claude Fable 5.1 --- docs/dev/debt/libSceAjm.native.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index d37af30120..14b4f3a476 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -20,7 +20,7 @@ - [sceAjmBatchJobControlBufferRa, sceAjmBatchJobRunBufferRa, sceAjmBatchJobRunSplitBufferRa, sceAjmBatchStartBuffer](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - the batch layout (chunk kinds, 20-bit payload, flags bits 32-51 as the flags chunk's payload, the builders' chunk order) follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/854b291c63360ff24e590809aff54cf5ab2dd4c9/src/core/libraries/ajm/ajm_batch.cpp) and was confirmed byte for byte for 64-byte control and 80-byte run jobs by a PS4 trace of Knowledge is Power: Decades (CUSA12435, Wwise, 32 545 batches dumped by a GoldHEN plugin). The console's builders write only bits 0-3 (kind), bit 4 (a buffer chunk's output direction) and bits 6-25 (payload) of the header word and leave bits 5, 26, 27 and 31 as the previous batch left them, so the kind is read as 4 bits plus the direction bit, not as 6 bits - [sceAjmBatchStartBuffer](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - the statistics instance 0x80000 and the inline chunk come from shadPS4 only: the traced title builds every batch with the builders and a return address, and no title was seen writing an inline chunk. Flags are kept as passed. Unknown chunk kinds, control flags other than RESET, INITIALIZE and SIDEBAND_GAPLESS_DECODE, control jobs with run buffers and run jobs with a control input throw - [sceAjmBatchStartBuffer refusals](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - probe batches through the console's builders on a PS4 showed that a run buffer chunk of size zero is dropped whatever its address (the job runs as without it: 0x10 and nothing consumed for no room, 0 for no input), and that a null address with a nonzero size refuses the batch before any of its jobs runs, with 0x80930011 returned and the error record holding 0x80930016, the job chunk's address, the chunk's offset in the job and the job's return address. Control buffer chunks follow the same rule here, not measured. A size not a multiple of 8, a chunk that overruns its job or the batch, or a job without flags are refused the same way with 0x80930011 in the record; the console's report for those was not measured -- [sceAjmBatchStartBuffer null and empty batches](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - a null batch address returns 0x80930005 whatever the size, as the PS5 builders' sceAjmBatchInitialize does for a null buffer, and a batch of zero bytes is accepted, runs nothing and completes at once; neither was measured on a console (the probe that tried a null batch hung the probe app before writing a result) +- [sceAjmBatchStartBuffer null and empty batches](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - a null batch address returns 0x80930005 whatever the size, as the PS5 builders' sceAjmBatchInitialize does for a null buffer, and a batch of zero bytes is accepted, runs nothing and completes at once; neither was measured on a console (the probe that passed a null batch froze the console before a result was written, so it is not retried on hardware) - [ATRAC9 run results](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - probe runs of a title's ATRAC9 stream through the console's builders on a PS4 showed the run result naming the side that ran out: `PARTIAL_INPUT` (0x8) when the input ran out with room left, on a superframe boundary or not; `NOT_ENOUGH_ROOM` (0x10) when the room ran out with input left, after any number of frames, and with nothing consumed for a zero room; both (0x18) when the input ended just as the output filled, which is every Wwise run job. ATRAC9 run jobs with `RUN_MULTIPLE_FRAMES` follow those rules here, vibration streams included, which were not traced; resampled runs and the other codecs keep the earlier rule (a trailing partial frame is partial input, a run that produced nothing is out of room) - [ATRAC9 run without RUN_MULTIPLE_FRAMES](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the same PS4 probe decoded exactly one frame and reported 0 for such a run; here it still decodes its whole input, as FMOD in PPSA29141 needs (it puts two run jobs in one batch with the second's input right after the whole of the first's), so a PS4 title that relies on one frame per run would get more; MP3 and Opus run jobs still stop after one frame. The console also rejected the tests' synthetic silent superframe after three frames (`CODEC_ERROR | FATAL`, internal result -577) where LibAtrac9 decodes four, so that vector proves job plumbing, not decoding; on the title's stream LibAtrac9 matched the console's PCM within 4 LSB, 83.5% of the samples exactly From 8c9c50b0b2d4add1532ffa9d021c00b9d54e42c1 Mon Sep 17 00:00:00 2001 From: ntamas94 Date: Fri, 23 Oct 2026 02:19:16 +0200 Subject: [PATCH 412/428] fix(libSceAjm): decode one ATRAC9 frame per PS4 run job without RUN_MULTIPLE_FRAMES A probe on a PS4 decoded exactly one frame and reported 0 for an ATRAC9 run job without RUN_MULTIPLE_FRAMES. Jobs read from a PS4 batch buffer now do the same. Jobs from the PS5 builders keep decoding their whole input, which FMOD in PPSA29141 relies on. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAjm.native/src/Ajm.cpp | 3 +++ core/libs/tests/GuestAjm.cpp | 23 ++++++++++++++++++++++ docs/dev/debt/libSceAjm.native.md | 2 +- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index deb893d345..72284a8281 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -218,6 +218,7 @@ struct JobHeader { std::uint32_t inputCount; std::uint32_t outputCount; std::uint8_t parameters[16]; + bool console; }; struct PackedJob { @@ -817,6 +818,7 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c ++frames; EmitFrame(instance, pcmOutputs, pcm.data(), frameSamples, channels, sampleBytes); + if (job.console && (job.flags & RUN_MULTIPLE_FRAMES) == 0) break; } if (segmentEnded()) instance.segmentSamples = 0; @@ -1808,6 +1810,7 @@ struct ConsoleRefusal { bool ReadConsoleJob(std::uint32_t instance, const std::uint8_t* job, const std::uint8_t* end, ConsoleJob& out, ConsoleRefusal& refusal) { out.header = JobHeader{}; out.header.instance = instance; + out.header.console = true; std::vector inputs; std::vector outputs; AjmBuffer controlInput{}; diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 894715ca9a..32522e33fd 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -2096,6 +2096,28 @@ void TestConsoleEmptyBatch(std::uint32_t context) { Require(sceAjmBatchWait(context, id, 0, &error) == invalidBatch); } +void TestConsoleSingleFrameRun(std::uint32_t context) { + constexpr std::uint64_t version = 1; + constexpr std::uint64_t sidebandStream = 1ull << 47; + std::vector stream(1024, 0); + std::memcpy(stream.data(), AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME)); + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0); + const At9Control start{{0, 0, 0}, {0xFE, 0x70, 0x1F, 0xF0}, 0}; + std::int32_t result[2] = {-1, -1}; + std::vector pcm(1024, 0x55); + DecodeSideband single{-1, -1, 0, 0, 0}; + std::vector batch(4096, 0xCD); + void* at = sceAjmBatchJobControlBufferRa(batch.data(), instance, version | CONTROL_START, &start, sizeof(start), result, sizeof(result), nullptr); + at = sceAjmBatchJobRunBufferRa(at, instance, version | sidebandStream, stream.data(), stream.size(), pcm.data(), pcm.size() * sizeof(std::int16_t), &single, sizeof(single), nullptr); + Require(StartBuffer(context, batch.data(), static_cast(at) - batch.data()) == 0); + Require(result[0] == 0 && single.result == 0 && single.inputConsumed > 0 && single.inputConsumed < 1024); + Require(single.outputWritten == 256 * 2 && single.totalDecodedSamples == 256); + Require(std::all_of(pcm.begin(), pcm.begin() + 256, [](std::int16_t sample) { return sample == 0; })); + Require(std::all_of(pcm.begin() + 256, pcm.end(), [](std::int16_t sample) { return sample == 0x55; })); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + void TestConsoleStaleHeaderBits(std::uint32_t context) { std::vector stream(1024, 0); std::memcpy(stream.data(), AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME)); @@ -2383,6 +2405,7 @@ int main() { TestConsoleHandWrittenJobs(context); TestConsoleBufferEdges(context); TestConsoleEmptyBatch(context); + TestConsoleSingleFrameRun(context); TestConsoleStaleHeaderBits(context); Require(sceAjmFinalize(context) == 0); } diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index 14b4f3a476..b955d26688 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -22,7 +22,7 @@ - [sceAjmBatchStartBuffer refusals](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - probe batches through the console's builders on a PS4 showed that a run buffer chunk of size zero is dropped whatever its address (the job runs as without it: 0x10 and nothing consumed for no room, 0 for no input), and that a null address with a nonzero size refuses the batch before any of its jobs runs, with 0x80930011 returned and the error record holding 0x80930016, the job chunk's address, the chunk's offset in the job and the job's return address. Control buffer chunks follow the same rule here, not measured. A size not a multiple of 8, a chunk that overruns its job or the batch, or a job without flags are refused the same way with 0x80930011 in the record; the console's report for those was not measured - [sceAjmBatchStartBuffer null and empty batches](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - a null batch address returns 0x80930005 whatever the size, as the PS5 builders' sceAjmBatchInitialize does for a null buffer, and a batch of zero bytes is accepted, runs nothing and completes at once; neither was measured on a console (the probe that passed a null batch froze the console before a result was written, so it is not retried on hardware) - [ATRAC9 run results](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - probe runs of a title's ATRAC9 stream through the console's builders on a PS4 showed the run result naming the side that ran out: `PARTIAL_INPUT` (0x8) when the input ran out with room left, on a superframe boundary or not; `NOT_ENOUGH_ROOM` (0x10) when the room ran out with input left, after any number of frames, and with nothing consumed for a zero room; both (0x18) when the input ended just as the output filled, which is every Wwise run job. ATRAC9 run jobs with `RUN_MULTIPLE_FRAMES` follow those rules here, vibration streams included, which were not traced; resampled runs and the other codecs keep the earlier rule (a trailing partial frame is partial input, a run that produced nothing is out of room) -- [ATRAC9 run without RUN_MULTIPLE_FRAMES](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the same PS4 probe decoded exactly one frame and reported 0 for such a run; here it still decodes its whole input, as FMOD in PPSA29141 needs (it puts two run jobs in one batch with the second's input right after the whole of the first's), so a PS4 title that relies on one frame per run would get more; MP3 and Opus run jobs still stop after one frame. The console also rejected the tests' synthetic silent superframe after three frames (`CODEC_ERROR | FATAL`, internal result -577) where LibAtrac9 decodes four, so that vector proves job plumbing, not decoding; on the title's stream LibAtrac9 matched the console's PCM within 4 LSB, 83.5% of the samples exactly +- [ATRAC9 run without RUN_MULTIPLE_FRAMES](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the same PS4 probe decoded exactly one frame and reported 0 for such a run, and a job read from a PS4 batch buffer (sceAjmBatchStartBuffer) does the same here. A job from the PS5 builders still decodes its whole input, as FMOD in PPSA29141 needs (it puts two run jobs in one batch with the second's input right after the whole of the first's); whether a PS5 stops after one frame is not measured. MP3 and Opus run jobs stop after one frame on both paths. The console also rejected the tests' synthetic silent superframe after three frames (`CODEC_ERROR | FATAL`, internal result -577) where LibAtrac9 decodes four, so that vector proves job plumbing, not decoding; on the title's stream LibAtrac9 matched the console's PCM within 4 LSB, 83.5% of the samples exactly ### Functional From 43c82acb0f2d08eaa9bcac4e682e1fa99b9fefdf Mon Sep 17 00:00:00 2001 From: ntamas94 Date: Sat, 24 Oct 2026 02:08:19 +0200 Subject: [PATCH 413/428] docs(libSceAjm): say the empty PS4 batch contract needs a non-null address A null batch address is refused whatever the size, so the accepted batch of zero bytes is one at a real address, which is what TestConsoleEmptyBatch starts. Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libSceAjm.native.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index b955d26688..5c693e0f85 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -20,7 +20,7 @@ - [sceAjmBatchJobControlBufferRa, sceAjmBatchJobRunBufferRa, sceAjmBatchJobRunSplitBufferRa, sceAjmBatchStartBuffer](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - the batch layout (chunk kinds, 20-bit payload, flags bits 32-51 as the flags chunk's payload, the builders' chunk order) follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/854b291c63360ff24e590809aff54cf5ab2dd4c9/src/core/libraries/ajm/ajm_batch.cpp) and was confirmed byte for byte for 64-byte control and 80-byte run jobs by a PS4 trace of Knowledge is Power: Decades (CUSA12435, Wwise, 32 545 batches dumped by a GoldHEN plugin). The console's builders write only bits 0-3 (kind), bit 4 (a buffer chunk's output direction) and bits 6-25 (payload) of the header word and leave bits 5, 26, 27 and 31 as the previous batch left them, so the kind is read as 4 bits plus the direction bit, not as 6 bits - [sceAjmBatchStartBuffer](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - the statistics instance 0x80000 and the inline chunk come from shadPS4 only: the traced title builds every batch with the builders and a return address, and no title was seen writing an inline chunk. Flags are kept as passed. Unknown chunk kinds, control flags other than RESET, INITIALIZE and SIDEBAND_GAPLESS_DECODE, control jobs with run buffers and run jobs with a control input throw - [sceAjmBatchStartBuffer refusals](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - probe batches through the console's builders on a PS4 showed that a run buffer chunk of size zero is dropped whatever its address (the job runs as without it: 0x10 and nothing consumed for no room, 0 for no input), and that a null address with a nonzero size refuses the batch before any of its jobs runs, with 0x80930011 returned and the error record holding 0x80930016, the job chunk's address, the chunk's offset in the job and the job's return address. Control buffer chunks follow the same rule here, not measured. A size not a multiple of 8, a chunk that overruns its job or the batch, or a job without flags are refused the same way with 0x80930011 in the record; the console's report for those was not measured -- [sceAjmBatchStartBuffer null and empty batches](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - a null batch address returns 0x80930005 whatever the size, as the PS5 builders' sceAjmBatchInitialize does for a null buffer, and a batch of zero bytes is accepted, runs nothing and completes at once; neither was measured on a console (the probe that passed a null batch froze the console before a result was written, so it is not retried on hardware) +- [sceAjmBatchStartBuffer null and empty batches](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm, PS4) - a null batch address returns 0x80930005 whatever the size, as the PS5 builders' sceAjmBatchInitialize does for a null buffer, and a batch of zero bytes at a non-null address is accepted, runs nothing and completes at once (a null address with size 0 is refused like any null address); neither was measured on a console (the probe that passed a null batch froze the console before a result was written, so it is not retried on hardware) - [ATRAC9 run results](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - probe runs of a title's ATRAC9 stream through the console's builders on a PS4 showed the run result naming the side that ran out: `PARTIAL_INPUT` (0x8) when the input ran out with room left, on a superframe boundary or not; `NOT_ENOUGH_ROOM` (0x10) when the room ran out with input left, after any number of frames, and with nothing consumed for a zero room; both (0x18) when the input ended just as the output filled, which is every Wwise run job. ATRAC9 run jobs with `RUN_MULTIPLE_FRAMES` follow those rules here, vibration streams included, which were not traced; resampled runs and the other codecs keep the earlier rule (a trailing partial frame is partial input, a run that produced nothing is out of room) - [ATRAC9 run without RUN_MULTIPLE_FRAMES](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the same PS4 probe decoded exactly one frame and reported 0 for such a run, and a job read from a PS4 batch buffer (sceAjmBatchStartBuffer) does the same here. A job from the PS5 builders still decodes its whole input, as FMOD in PPSA29141 needs (it puts two run jobs in one batch with the second's input right after the whole of the first's); whether a PS5 stops after one frame is not measured. MP3 and Opus run jobs stop after one frame on both paths. The console also rejected the tests' synthetic silent superframe after three frames (`CODEC_ERROR | FATAL`, internal result -577) where LibAtrac9 decodes four, so that vector proves job plumbing, not decoding; on the title's stream LibAtrac9 matched the console's PCM within 4 LSB, 83.5% of the samples exactly From b166232c722f77e3616dbed5e24a579e8be7d3bf Mon Sep 17 00:00:00 2001 From: Davi Azevedo Date: Fri, 23 Oct 2026 18:11:33 -0300 Subject: [PATCH 414/428] fix(agc): refresh DCC sampled array windows --- .../Graphics/include/TextureTiling.hpp | 2 +- .../Graphics/src/ShaderResources.cpp | 9 ++- .../Graphics/src/TextureTiling.cpp | 17 +++--- .../libSceAgcDriver/tests/TextureTiling.cpp | 57 ++++++++++++++++--- docs/dev/debt/libSceAgcDriver.md | 2 +- 5 files changed, 70 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp index 5699bf78e1..dafa52d5f5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp @@ -75,7 +75,7 @@ struct SurfaceGeometry { }; SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor); bool LevelsFitAllocation(const GuestTextureResource& surface, std::uint32_t levels); -GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed); +GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed, bool keysReadAsStored); GuestTextureResource ArrayLayerWindow(const GuestTextureResource& viewed, std::uint32_t maxArrayLayers); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index f73b2c2b0e..5aa05a85b0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -752,7 +752,14 @@ std::shared_ptr cachedTexture(const Context& context, std::span windows{0}; const auto count = windows.fetch_add(1) + 1; if (count <= 5 || (count & (count - 1)) == 0) std::fprintf(stderr, "[gpu] sampled array view 0x%llx (%ux%u format %u, dimension %d, slices %u..%u) served from its viewed slices at 0x%llx; %llu such views\n", static_cast(resource.baseAddress), resource.width, resource.height, resource.format, static_cast(resource.dimension), resource.baseArray, resource.depthOrLastArray, static_cast(window.baseAddress), static_cast(count)); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp index ff1bed0787..494c38359e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp @@ -466,23 +466,26 @@ bool LevelsFitAllocation(const GuestTextureResource& surface, std::uint32_t leve return true; } -GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed) { +GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed, bool keysReadAsStored) { const bool layered = viewed.dimension == TextureDimension::k2DArray || viewed.dimension == TextureDimension::k1DArray || viewed.dimension == TextureDimension::kCube; - if (!layered || viewed.baseArray == 0 || viewed.baseArray > viewed.depthOrLastArray || viewed.dccAddress != 0) return viewed; + if (!layered || viewed.baseArray == 0 || viewed.baseArray > viewed.depthOrLastArray || (viewed.dccAddress != 0 && !keysReadAsStored)) return viewed; + std::uint32_t sliceMask = 0; if (const auto xorMode = XorSwizzleMode(viewed.tileMode); xorMode != 0u) { const auto* equation = FindTextureSwizzleEquation(xorMode, BytesPerElement(viewed.format)); if (equation == nullptr) return viewed; std::uint32_t sliceBits = 0; for (const auto bit : equation->bits) sliceBits |= bit >> 24u; - const auto sliceMask = sliceBits == 0 ? 0u : (1u << std::bit_width(sliceBits)) - 1u; - if ((viewed.baseArray & sliceMask) != 0) return viewed; + sliceMask = sliceBits == 0 ? 0u : (1u << std::bit_width(sliceBits)) - 1u; } + const auto first = viewed.baseArray & ~sliceMask; + if (first == 0) return viewed; const auto geometry = DescribeSurface(viewed); if (geometry.thick || geometry.layers != viewed.depthOrLastArray + 1u) return viewed; auto window = viewed; - window.baseAddress = viewed.baseAddress + geometry.GuestLayerOffset(viewed.baseArray); - window.depthOrLastArray = viewed.depthOrLastArray - viewed.baseArray; - window.baseArray = 0; + window.baseAddress = viewed.baseAddress + geometry.GuestLayerOffset(first); + window.depthOrLastArray = viewed.depthOrLastArray - first; + window.baseArray = viewed.baseArray - first; + window.dccAddress = 0; return window; } diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp index 48b7f6fa98..fce3aa0322 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp @@ -1,4 +1,6 @@ #include "GraphicsTests.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" @@ -424,26 +426,35 @@ void RunTextureTilingTests() { array.depthOrLastArray = 1536; array.baseArray = 1536; const auto layerBytes = DescribeSurface(array).layerBytes; - const auto window = ViewedArrayWindow(array); + const auto window = ViewedArrayWindow(array, false); Require(window.baseAddress == array.baseAddress + 1536ull * layerBytes && window.depthOrLastArray == 0 && window.baseArray == 0, "a sampled view of one slice at 1536 of an R_X array must start at that slice"); Require(DescribeSurface(window).layerBytes == layerBytes && DescribeSurface(window).layers == 1, "a sampled array window must keep the slice layout"); auto unaligned = array; unaligned.baseArray = 1530; - Require(ViewedArrayWindow(unaligned).baseAddress == array.baseAddress, "an R_X window at a slice the swizzle mixes in must keep the whole array"); + const auto mixed = ViewedArrayWindow(unaligned, false); + Require(mixed.baseAddress == array.baseAddress + 1520ull * layerBytes && mixed.baseArray == 10 && mixed.depthOrLastArray == 16, "an R_X view at a slice the swizzle mixes in must start its window at the last slice before it with those bits clear"); + auto low = array; + low.baseArray = 9; + Require(ViewedArrayWindow(low, false).baseAddress == array.baseAddress, "an R_X view below the first slice with those bits clear keeps the whole array"); auto plain = array; plain.tileMode = TextureTileMode::kStandard64KB; plain.baseArray = 1530; - Require(ViewedArrayWindow(plain).baseAddress == array.baseAddress + 1530ull * DescribeSurface(plain).layerBytes && ViewedArrayWindow(plain).depthOrLastArray == 6, "a non-XOR window may start at any slice"); + Require(ViewedArrayWindow(plain, false).baseAddress == array.baseAddress + 1530ull * DescribeSurface(plain).layerBytes && ViewedArrayWindow(plain, false).depthOrLastArray == 6, "a non-XOR window may start at any slice"); auto fromZero = array; fromZero.baseArray = 0; - Require(ViewedArrayWindow(fromZero).baseAddress == array.baseAddress && ViewedArrayWindow(fromZero).depthOrLastArray == 1536, "a view from slice 0 keeps its descriptor"); + Require(ViewedArrayWindow(fromZero, false).baseAddress == array.baseAddress && ViewedArrayWindow(fromZero, false).depthOrLastArray == 1536, "a view from slice 0 keeps its descriptor"); auto compressed = array; compressed.dccAddress = 0x20000000ull; - Require(ViewedArrayWindow(compressed).baseAddress == array.baseAddress, "a view with DCC metadata keeps its descriptor"); + Require(ViewedArrayWindow(compressed, false).baseAddress == array.baseAddress, "a view with DCC metadata keeps its descriptor"); + const auto stored = ViewedArrayWindow(compressed, true); + Require(stored.baseAddress == window.baseAddress && stored.depthOrLastArray == 0 && stored.baseArray == 0 && stored.dccAddress == 0, "a view whose DCC keys read as stored is cut to its slices without metadata"); + auto compressedFromZero = compressed; + compressedFromZero.baseArray = 0; + Require(ViewedArrayWindow(compressedFromZero, true).baseAddress == array.baseAddress && ViewedArrayWindow(compressedFromZero, true).dccAddress == compressed.dccAddress, "a view with DCC metadata from slice 0 keeps its descriptor"); GuestTextureResource cube{}; cube.baseAddress = 0x30000000ull; @@ -455,13 +466,45 @@ void RunTextureTilingTests() { cube.tileMode = TextureTileMode::kStandard4KB; cube.depthOrLastArray = 7232; cube.baseArray = 7227; - const auto faces = ViewedArrayWindow(cube); + const auto faces = ViewedArrayWindow(cube, false); Require(faces.baseAddress == cube.baseAddress + 7227ull * DescribeSurface(cube).layerBytes && faces.depthOrLastArray == 5 && faces.baseArray == 0 && faces.dimension == TextureDimension::kCube, "a cube view of six faces at 7227 must start at its first face"); + auto compressedVolume = cube; + compressedVolume.dimension = TextureDimension::k3D; + compressedVolume.baseArray = 0; + compressedVolume.dccAddress = 0x20000000ull; + Require(ViewedArrayWindow(compressedVolume, true).dccAddress == compressedVolume.dccAddress, "a volume with DCC metadata keeps its descriptor"); + auto volume = cube; volume.dimension = TextureDimension::k3D; volume.baseArray = 0; - Require(ViewedArrayWindow(volume).baseAddress == cube.baseAddress, "a volume keeps its descriptor"); + Require(ViewedArrayWindow(volume, false).baseAddress == cube.baseAddress, "a volume keeps its descriptor"); + } + + { + const std::array words{0x027c0000u, 0xc1600000u, 0x00ffc0ffu, 0xd1800924u, 0x027cc27cu, 0xc1400000u, 0x00704018u, 0x91b00204u}; + const auto view = DecodeTextureResource(words); + Require(view.baseAddress == 0x27c000000ull && view.dccAddress == 0x91b002040000ull && view.dimension == TextureDimension::k2DArray && view.baseArray == 636 && view.depthOrLastArray == 636 && view.width == 1024 && view.height == 1024 && view.format == 22, "the title's DCC array view decodes to slice 636 of 637"); + const auto whole = DescribeSurface(view); + Require(whole.guestBytes == 2671771648ull, "the uncut DCC array view covers all 637 slices (" + std::to_string(whole.guestBytes) + " bytes)"); + Require(TextureClearKeys(view, whole.guestBytes) == DccKeys::Uncompressed, "unreadable DCC keys of the uncut view read the texels as stored"); + Require(ViewedArrayWindow(view, false).baseAddress == view.baseAddress, "a DCC view whose keys are not known to read as stored keeps its descriptor"); + const auto window = ViewedArrayWindow(view, true); + const auto cut = DescribeSurface(window); + Require(window.baseAddress == view.baseAddress + whole.GuestLayerOffset(624) && window.dccAddress == 0 && window.baseArray == 12 && window.depthOrLastArray == 12, "the window of a stored-keys DCC view starts at the slice-swizzle boundary below its slice without metadata"); + Require(cut.layers == 13 && cut.layerBytes == whole.layerBytes && window.baseAddress + cut.guestBytes == view.baseAddress + whole.guestBytes, "the window covers slices 624..636 (" + std::to_string(cut.guestBytes) + " bytes)"); + for (std::uint32_t slice = 0; slice < cut.layers; ++slice) Require(window.baseAddress + cut.GuestLayerOffset(slice) == view.baseAddress + whole.GuestLayerOffset(624 + slice), "a window slice lies where the uncut surface keeps it"); + const auto* equation = FindTextureSwizzleEquation(XorSwizzleMode(view.tileMode), 4); + const auto block = ThinBlockLayout(view.tileMode, 4); + Require(equation != nullptr && whole.mips.size() == 1 && !whole.mips[0].tail, "the title's DCC array is one untailed Z_X level"); + const auto texel = [&](const GuestTextureResource& surface, const SurfaceGeometry& geometry, std::uint32_t x, std::uint32_t y, std::uint32_t slice) { + const auto& mip = geometry.mips[0]; + return surface.baseAddress + geometry.GuestLayerOffset(slice) + mip.tiledOffset + (static_cast(y / block[2]) * mip.blocksPerRow + x / block[1]) * block[0] + equationOffset(*equation, x, y, slice); + }; + for (std::uint32_t y = 0; y < 1024; y += 7) { + for (std::uint32_t x = 0; x < 1024; x += 5) Require(texel(window, cut, x, y, window.baseArray) == texel(view, whole, x, y, view.baseArray), "a texel of the viewed slice detiles from the same guest byte in the window as in the uncut surface"); + } + Require(TextureClearKeys(window, cut.guestBytes) == DccKeys::Uncompressed, "the window reads its texels as stored, as the uncut view does"); } { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index e715b5b83e..6dc0708155 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -260,7 +260,7 @@ - [Viewport depth range remap](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - on a device without `VK_EXT_depth_range_unrestricted` a viewport depth range outside [0, 1] (Minecraft PPSA17221 draws its world with `PA_CL_VPORT_ZSCALE` 1, `ZOFFSET` 0 in negative-one-to-one clip space) is folded into the vertex shader: `RemapPositionDepth` scales clip z so z/w is the console's depth, the pipeline's depth clamp applies the console's `PA_SC_VPORT_ZMIN/ZMAX` clipped to [0, 1] (exact for a normalized depth target or none; Minecraft clamps its float depth target to [-1, 1], so a depth below 0 is stored as 0 there, which only a later test or read at exactly 0 can tell, and the first such draw is reported once), and two extra `gl_ClipDistance` elements restore the near and far planes depth clamping removes. The mode is a pipeline specialization constant (`PipelineSpecialization::DepthRemapBase`); scale and offset are read from the draw's push block (two words at byte 104, `DepthRemapPushOffsetBytes`, so a remapped draw's stage push data ends before it), so draws that differ only in them share one vertex module and one pipeline. The remap serves the vertex path only; mesh and tessellation draws with such a range still throw. Whether the console clamps before or after its depth bias and how it treats a clamp range narrower than the transform's are not measured. - [Benchmark rewrite failure test off Windows](../../../core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp) (libSceAgcDriver) - `ReplaceGzipFile` checks that a write failing at `.tmp` keeps the previous `aps5-bench.json.gz` only on non-Windows builds: the `std::ios_base::failure` thrown inside the MinGW ofstream did not unwind to the test's catch, so Windows CI covers only the failed move. -- [Sampled array windows](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) (libSceAgcDriver) - a sampled 2D array, 1D array or cube view whose base array slice is past 0 is built from its viewed slices only (`ViewedArrayWindow`), at the guest address of its base slice. The window is taken only when the surface is thin, has no DCC metadata, and, for a 64 KiB XOR swizzle, when the base slice keeps every slice bit of the swizzle equation at zero (R_X 1-byte elements: a base slice that is a multiple of 16). Any other view still builds slices 0 to its last slice. A window and a whole-array view of the same surface are separate images over overlapping memory, kept coherent by the existing overlap rules, at the cost of the memory and uploads of both. PPSA26344 samples a 256x256 cube array at slices 4299..7232, built at 1467 MiB instead of about 3.8 GB. +- [Sampled array windows](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) (libSceAgcDriver) - a sampled 2D array, 1D array or cube view past slice 0 is built from its viewed slices only (`ViewedArrayWindow`). The window needs a thin surface without DCC metadata, or with keys that resolve over the whole surface to Uncompressed (the window then drops the metadata; keys are rescanned at each lookup). For a 64 KiB XOR swizzle it starts at the last slice at or before the base whose swizzle slice bits are zero (R_X 1-byte and Z_X 4-byte: a multiple of 16); when that is slice 0 the view builds slices 0 to its last slice. A window and a whole-array view of the same surface are separate images over overlapping memory, kept coherent by the existing overlap rules, at the cost of both. PPSA26344 samples a cube array at slices 4299..7232 at 1467 MiB instead of about 3.8 GB; PPSA01341 samples slice 636 of a DCC 2D array at 52 MiB instead of 2.5 GiB. - [2D reads of 2D arrays](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled image whose shader shape is 2D and whose T# is a 2D array is built from its base slice alone: the T# is narrowed to that slice (`FirstLayerDescriptor`) before its decode, cache key and image, in stage A records, stage B bindings, revalidation and image tables. A whole-array read of the same T# keeps its own cache entry, so both images exist when a title uses both. Storage images read or written through a 2D shape are not narrowed and still build every slice the T# names. PPSA26344 reads a 3328x1872 R_X T# of 1537 slices this way: the whole array was a 12.4 GiB image that did not fit the device-local budget of an RTX 4090. - [Sampled and storage views of 1 GiB or more](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) that stay whole are only logged (sampled views by `cachedTexture`, storage views by `cachedStorageTexture`), with the registered guest range holding their base and how many of their slices fit in it; nothing is capped to that range. PPSA26344 samples a 1467 MiB cube array window of which only 838 of 2934 slices lie inside its 3380 MiB range: the rest is read from whatever follows the range. Whether such slices are ever sampled is not measured. - [Color export type against the target format](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - the export-format check refuses a `UINT16_ABGR` export (`SPI_SHADER_COL_FORMAT` 7) into a target whose color-buffer number format is not unsigned integer, but not the reverse. A 32-bit export (`32_R`, `32_GR`, `32_AR` or `32_ABGR`) into an unsigned-integer target leaves `uintExport` clear, so the recompiler declares the fragment output `float4`; `DecodeColorBuffer` maps a number format of 4 to `R32_UINT` from the color buffer alone, independent of the export format, so the float output reaches an integer attachment. Desktop drivers keep the raw bits, but the Khronos validation layer reports `Undefined-Value-ShaderFragmentOutputMismatch` and MoltenVK's Metal compiler refuses the pipeline. Which export format a title pairs with a 32-bit integer target is not measured, so the output type is left following the export format rather than the attachment. From 8065d65f01c8494d3a0bd10e4afea9e08b20ecd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 25 Oct 2026 18:13:38 +0100 Subject: [PATCH 415/428] fix(libSceAjm): declare each batch export in one header The public header audit found the three PS4 batch exports declared in libSceAjm's Api.hpp and in libSceAjm.native's, and the libSceAjmi run stub under its plain name. Keep them in libSceAjm.native and declare the libSceAjmi stub as sceAjmBatchJobRunBufferRa_nid_disambig1, as its definition is. Co-Authored-By: Claude Haiku 5.5 --- core/libs/prx/libSceAjm/include/Api.hpp | 4 ---- core/libs/prx/libSceAjmi/include/Api.hpp | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAjm/include/Api.hpp b/core/libs/prx/libSceAjm/include/Api.hpp index 1be3b9a23e..2cc01a795b 100644 --- a/core/libs/prx/libSceAjm/include/Api.hpp +++ b/core/libs/prx/libSceAjm/include/Api.hpp @@ -4,8 +4,4 @@ extern "C" { -int APS5_VABI sceAjmBatchJobControlBufferRa(); -int APS5_VABI sceAjmBatchJobRunSplitBufferRa(); -int APS5_VABI sceAjmBatchStartBuffer(); - } diff --git a/core/libs/prx/libSceAjmi/include/Api.hpp b/core/libs/prx/libSceAjmi/include/Api.hpp index cf36eb1119..f12efb729c 100644 --- a/core/libs/prx/libSceAjmi/include/Api.hpp +++ b/core/libs/prx/libSceAjmi/include/Api.hpp @@ -5,6 +5,6 @@ extern "C" { int APS5_VABI sceAjmBatchJobInlineBuffer(void); -int APS5_VABI sceAjmBatchJobRunBufferRa(void); +int APS5_VABI sceAjmBatchJobRunBufferRa_nid_disambig1(void); } From 1adc418e6e9c243c4bf071e76092202b93b2e969 Mon Sep 17 00:00:00 2001 From: Eduardo Ruiz Date: Sat, 24 Oct 2026 16:13:11 +0200 Subject: [PATCH 416/428] feat(libc): forward pthread attributes in _Pad::_Launch --- core/libs/prx/libc/include/Api.hpp | 1 + core/libs/prx/libc/src/Threads.cpp | 12 ++++++++-- core/libs/tests/GuestLibcThreads.cpp | 33 +++++++++++++++++++++++++++- docs/dev/debt/libc.md | 2 +- 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index d25dbadc88..accb894860 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -1010,6 +1010,7 @@ void APS5_VABI _ZNSt4_PadC2Ev_nid_postfix(GuestPad* pad); void APS5_VABI _ZNSt4_PadD2Ev_nid_postfix(GuestPad* pad); void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(GuestPad* pad); void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, void** thread); +void APS5_VABI _ZNSt4_Pad7_LaunchEPP12pthread_attrPP7pthread_nid_postfix(GuestPad* pad, void** attrSlot, void** thread); int APS5_VABI _Thrd_join_nid_postfix(void* thread, int* code); int APS5_VABI _Thrd_detach_nid_postfix(void* thread); void* APS5_VABI _Thrd_id_nid_postfix(void); diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index ffa49e1ea7..cfadc7da6d 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -464,15 +464,23 @@ void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(GuestPad* pad) { RequireThrd(_Mtx_unlock_nid_postfix(&pad->mutex), "_Pad::_Release: _Mtx_unlock"); } -void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, void** thread) { +static void LaunchPad_nid_no_patch(GuestPad* pad, void** attrSlot, void** thread) { if (!pad || !thread) APS5_INVALID_ARG_EX; const auto create = registeredPthreadCreate.load(std::memory_order_acquire); if (!create) throw std::runtime_error("_Pad::_Launch: pthread_create is not registered"); - const int created = create(thread, nullptr, PadEntry_nid_no_patch, pad); + const int created = create(thread, attrSlot, PadEntry_nid_no_patch, pad); if (created != 0) ThrowCppError_nid_no_patch(6); while (!pad->started) RequireThrd(_Cnd_wait_nid_postfix(&pad->condition, &pad->mutex), "_Pad::_Launch: _Cnd_wait"); } +void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, void** thread) { + LaunchPad_nid_no_patch(pad, nullptr, thread); +} + +void APS5_VABI _ZNSt4_Pad7_LaunchEPP12pthread_attrPP7pthread_nid_postfix(GuestPad* pad, void** attrSlot, void** thread) { + LaunchPad_nid_no_patch(pad, attrSlot, thread); +} + int APS5_VABI _Thrd_join_nid_postfix(void* thread, int* code) { const auto join = registeredPthreadJoin.load(std::memory_order_acquire); if (!join) throw std::runtime_error("_Thrd_join: pthread_join is not registered"); diff --git a/core/libs/tests/GuestLibcThreads.cpp b/core/libs/tests/GuestLibcThreads.cpp index a1f0a30703..2f72934b82 100644 --- a/core/libs/tests/GuestLibcThreads.cpp +++ b/core/libs/tests/GuestLibcThreads.cpp @@ -202,7 +202,10 @@ struct HostPadThread { void* result = nullptr; }; -static int APS5_VABI HostPthreadCreate(void** thread, const void*, void* (APS5_VABI* entry)(void*), void* arg) { +static const void* lastCreateAttrSlot = nullptr; + +static int APS5_VABI HostPthreadCreate(void** thread, const void* attrSlot, void* (APS5_VABI* entry)(void*), void* arg) { + lastCreateAttrSlot = attrSlot; auto* handle = new HostPadThread{}; handle->worker = std::thread([handle, entry, arg] { handle->result = entry(arg); }); *thread = handle; @@ -263,6 +266,7 @@ static void PadLaunchAndJoin() { _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(reinterpret_cast(&pad), &thread); Require(thread != nullptr && pad.started && pad.ran.load()); + Require(lastCreateAttrSlot == nullptr); _ZNSt4_PadD2Ev_nid_postfix(reinterpret_cast(&pad)); int code = 0; @@ -273,6 +277,32 @@ static void PadLaunchAndJoin() { RegisterPthreadJoin_nid_no_patch(nullptr); } +static void PadLaunchWithAttributes() { + unsigned (APS5_VABI* vtable[1])(GuestPad*) = {TestPadGo}; + RegisterPthreadCreate_nid_no_patch(HostPthreadCreate); + RegisterPthreadJoin_nid_no_patch(HostPthreadJoin); + + TestPad pad{}; + pad.vtable = vtable; + _ZNSt4_PadC2Ev_nid_postfix(reinterpret_cast(&pad)); + + int attribute = 0; + void* attrSlot = &attribute; + void* thread = nullptr; + lastCreateAttrSlot = nullptr; + _ZNSt4_Pad7_LaunchEPP12pthread_attrPP7pthread_nid_postfix(reinterpret_cast(&pad), &attrSlot, &thread); + Require(thread != nullptr && pad.started && pad.ran.load()); + Require(attrSlot == &attribute); + Require(lastCreateAttrSlot == &attrSlot); + + _ZNSt4_PadD2Ev_nid_postfix(reinterpret_cast(&pad)); + int code = 0; + Require(_Thrd_join_nid_postfix(thread, &code) == success && code == 42); + + RegisterPthreadCreate_nid_no_patch(nullptr); + RegisterPthreadJoin_nid_no_patch(nullptr); +} + static void PadDetach() { unsigned (APS5_VABI* vtable[1])(GuestPad*) = {TestPadGo}; RegisterPthreadDetach_nid_no_patch(nullptr); @@ -447,6 +477,7 @@ int main() { ConditionBroadcast(); ConditionSignal(); PadLaunchAndJoin(); + PadLaunchWithAttributes(); PadDetach(); ThreadHelpers(); } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 6baa225040..cabc255b2f 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -168,7 +168,7 @@ - [codecvt vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, NID `H-TDszhsYuY`) is thirteen zeroed `uintptr_t` slots matching `st_size` 104 on title `libc.prx` from PPSA26344 and on `libSceLibcInternal.sprx` (offset-to-top, typeinfo, two destructors, `_Incref`/`_Decref`, and seven conversion virtuals). Facet calls through the object fault until those methods are wired. - [num_put vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, NID `59oywaaZbJk`) is fourteen zeroed `uintptr_t` slots matching `st_size` 112 on the title `libc.prx` from PPSA16833 (offset-to-top, typeinfo, two destructors, `_Incref`/`_Decref`, and eight `do_put` virtuals). Facet calls through the object fault until those methods are wired. - [ios_base standard streams](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) `_Addstd` queues the stream in the standard stream table of Dinkumware [ios.cpp](https://github.com/ganboing/crt.vc12/blob/master/src/ios.cpp) (`stdstr`, `stdopens`, `_Addstd`, `_Ios_base_dtor`): it takes the first of slots 1 to 7 that is free or already holds the stream, and slot 8 (`NSTDSTR`) when none is, so slot 8 is shared by every stream past the first seven and registration never fails. It stores the 1-based slot index in the guest `IosBase::standardStream` (Dinkumware's `_Stdstr`, where 0 means not a standard stream) and counts the stream objects registered per slot; registering the same stream again reuses its slot and raises its count. Measured on a PS5 (firmware 13.60) from a homebrew payload calling the system `libSceLibcInternal` export: in a process whose own standard streams already hold slots 1 to 7, twelve distinct streams and a repeated one all get 8, written as a 64-bit value at offset 0x8 with no other byte of the object touched; the assignment of slots 1 to 7 to fresh streams is not observable there and follows VC12. The counts are `int`, not VC12's `char`, and the registration and the destructor lock the table where VC12 locks only `_Addstd`; a slot index past the table throws in the destructor instead of indexing out of bounds. The destructor decrements its slot's count and suppresses the rest of the destruction while the count stays positive, as `_Ios_base_dtor` does, and the slots of destroyed streams keep their pointer and stay reserved for that stream, as `stdstr` does. The streams themselves are destroyed by the title's own code as it registers them through `__cxa_atexit`; when the last reference closes, the unsupported stream storage and callback cleanup is still the throw it was and the locale is released as for a non-standard stream. -- [std::_Pad, _Thrd_join, _Thrd_detach, _Thrd_id](../../../core/libs/prx/libc/src/Threads.cpp) (libc) follow Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), otherwise without console verification. `_Thrd_id` returns `scePthreadSelf()` unchanged, as measured on a PS5 (firmware 13.40) in two threads (#4164, #8711); `_Launch` omits `_Cnd_do_broadcast_at_thread_exit` and calls `pthread_create` registered by libkernel with default attributes. The 32-byte pad layout (`_Go()` vtable at 0x0, `_Cnd_t` at 0x8, `_Mtx_t` at 0x10, `bool _Started` at 0x18) is assumed from VC12, unverified. Failing primitives or thread start throw the guest `std::system_error`; null arguments throw. `_Thrd_join` returns `_Thrd_error` (4) on `pthread_join` failure and truncates the exit code to `int`; `_Thrd_detach` returns `_Thrd_error` (4) on `pthread_detach` failure and throws when none is registered +- [std::_Pad, _Thrd_join, _Thrd_detach, _Thrd_id](../../../core/libs/prx/libc/src/Threads.cpp) (libc) follow Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), otherwise without console verification. `_Thrd_id` returns `scePthreadSelf()` unchanged, as measured on a PS5 (firmware 13.40) in two threads (#4164, #8711); `_Launch` omits `_Cnd_do_broadcast_at_thread_exit`; the two-argument form starts with default attributes and the three-argument form forwards the caller's attribute pointer, as PPSA02530's `libc.prx` does at `0x18090`. The 32-byte pad layout is assumed from VC12, unverified. Failing primitives or thread start throw the guest `std::system_error`; null arguments throw. `_Thrd_join` returns `_Thrd_error` (4) on `pthread_join` failure and truncates the exit code to `int`; `_Thrd_detach` returns `_Thrd_error` (4) on `pthread_detach` failure and throws when none is registered - [siginterrupt](../../../core/libs/prx/libc/src/Process.cpp) (libc) runs FreeBSD 11.4's `siginterrupt.c` sequence through the `sigaction` and the `_sigintr` set that libkernel registers at load: it reads the signal's disposition, sets or clears the signal's bit and `SA_RESTART`, and installs the disposition again. The set is written outside libkernel's registration lock, as FreeBSD's libc writes it without one; a call before libkernel has registered throws. - [collate vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, NID `ruZtIwbCFjk`) is nine zeroed `uintptr_t` slots matching `st_size` 72 on the title `libc.prx` from PPSA16833 (offset-to-top, typeinfo, two destructors, `_Incref`/`_Decref`, and three `do_*` virtuals). Facet calls through the object fault until those methods are wired; `_Getcat` still installs the host `g_collateVtable` for facets it builds. - [num_get::id, time_get::id and the num_get vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) are data objects, as the public SDK's libSceLibcInternal table declares them and as the other facet ids and the `num_put` and `codecvt` vtables are kept: the ids start at zero and the vtable is 17 zeroed slots, the 136-byte size of `KfcTPbeaOqg` in the console's libSceLibcInternal, whose relocations fill the offset-to-top and typeinfo entries, the two destructors, `locale::facet::_Incref` and `_Decref`, and the eleven `do_get` of Dinkumware's crt.vc12 `xlocnum`, so a guest resolves the symbols and reads data, and a facet call through the vtable faults. libSceAmpr exported the three as throwing functions, which a data consumer reads as code; its qualified imports fall back to libc. From 67c149465e00c9389d8489cc040ec863b7755822 Mon Sep 17 00:00:00 2001 From: Braxton Atkinson Date: Sat, 24 Oct 2026 23:29:08 -0500 Subject: [PATCH 417/428] feat(net): store DNS overrides and match firmware argument checks Apply the firmware 13.42 module-probe report in PR #9506: all four DNS calls check initialization first; getters return EINVAL for null outputs or nonzero flags; setters reject flags before reading guest memory. Keep the checked input copy and document EFAULT as host safety, not firmware behavior. Use the current public Net.hpp declarations in the regression. Rebased onto upstream e7e3e9c1984c293f06bff08428b8c89b7b932ac2. Ubuntu 24.04, GCC 13.3, Release: actual libc/libkernel/libSceNet libraries and both focused test executables built. guest_sce_net passes. A separate harness linked against those unmodified libraries passes initialization, errno, invalid flags, unreadable-input flag precedence, null clearing and unchanged-output checks. The full guest_net_dns_info test is blocked here: process_vm_readv is denied with EPERM by the execution environment. No full DNS regression pass is claimed. Public-header audit, conventions, test registration and whitespace checks pass. The resolver still uses host DNS; IPv6 host-server discovery and firmware server-count behavior remain debt. Firmware evidence: https://github.com/boykopovar/AnyPS5/pull/9506#issuecomment-6390574180 AI-assisted: yes --- core/libs/prx/libSceNet/CMakeLists.txt | 10 ++ core/libs/prx/libSceNet/Export.cpp | 45 +++++- .../Socket/include/SocketRuntime.hpp | 1 + core/libs/prx/libkernel/Socket/src/Udp.cpp | 4 + core/libs/tests/GuestNetDnsInfo.cpp | 139 ++++++++++++++++++ core/libs/tests/GuestSceNet.cpp | 10 +- docs/dev/debt/libSceNet.md | 3 +- 7 files changed, 199 insertions(+), 13 deletions(-) create mode 100644 core/libs/tests/GuestNetDnsInfo.cpp diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index eee438654e..11ebc91ea8 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -64,6 +64,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test_executable(guest_net_dns_info_tests ${LIBS_INCLUDE_DIR}/tests/GuestNetDnsInfo.cpp) + target_include_directories(guest_net_dns_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_net_dns_info_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(guest_net_dns_info_tests) + add_test(NAME guest_net_dns_info COMMAND guest_net_dns_info_tests) + set_tests_properties(guest_net_dns_info PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_net_dns_info PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_refused_connect_tests ${LIBS_INCLUDE_DIR}/tests/GuestRefusedConnect.cpp) target_include_directories(guest_refused_connect_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_refused_connect_tests PRIVATE libSceNet libkernel libc) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 3e70dd1d03..1fb564febf 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -356,6 +356,15 @@ int* errno_slot() { extern "C" std::size_t NetCtlHostDnsServers_nid_no_patch(std::uint32_t* servers, std::size_t capacity); +std::mutex g_dnsLock; +bool g_dnsOverridden = false; +NetDnsInfo g_dnsOverride{}; +NetDns6Info g_dns6Override{}; + +bool IsZeroAddress(const NetIpv6Address& address) { + return std::all_of(std::begin(address.bytes), std::end(address.bytes), [](std::uint8_t byte) { return byte == 0; }); +} + void set_errno(int err) { *errno_slot() = err; *__error_nid_postfix() = err; @@ -2262,7 +2271,14 @@ int APS5_VABI sceNetGetInterfaceStats(NetInterfaceStats* stats, std::uint32_t fl } int APS5_VABI sceNetSetDnsInfo(const NetDnsInfo* info, std::uint32_t flags) { - NotImplemented_nid_no_patch(__func__); + if (!g_net_inited) return fail(NET_ENOTINIT); + if (flags != 0) return fail(NET_EINVAL); + NetDnsInfo servers{}; + if (info && !GuestSockets::ReadGuest(&servers, info, sizeof(servers))) return fail(NET_EFAULT); + if (info && servers.primaryAddress == 0) return fail(NET_EINVAL); + std::lock_guard lock(g_dnsLock); + g_dnsOverridden = info != nullptr; + g_dnsOverride = servers; return 0; } @@ -2311,7 +2327,13 @@ int APS5_VABI sceNetResolverConnectDestroy(NetResolverConnection* connection) { } int APS5_VABI sceNetSetDns6Info(const NetDns6Info* info, std::uint32_t flags) { - NotImplemented_nid_no_patch(__func__); + if (!g_net_inited) return fail(NET_ENOTINIT); + if (flags != 0) return fail(NET_EINVAL); + NetDns6Info servers{}; + if (info && !GuestSockets::ReadGuest(&servers, info, sizeof(servers))) return fail(NET_EFAULT); + if (info && IsZeroAddress(servers.primaryAddress)) return fail(NET_EINVAL); + std::lock_guard lock(g_dnsLock); + g_dns6Override = servers; return 0; } @@ -2380,8 +2402,15 @@ int APS5_VABI sceNetGetAppMemInfo(NetAppMemoryInfo* info, std::uint32_t flags) { } int APS5_VABI sceNetGetDnsInfo(NetDnsInfo* info, std::uint32_t flags) { - if (flags != 0) throw std::runtime_error("sceNetGetDnsInfo: flags " + std::to_string(flags) + " are not supported"); - if (!info) throw std::runtime_error("sceNetGetDnsInfo: a null info is not supported"); + if (!g_net_inited) return fail(NET_ENOTINIT); + if (flags != 0 || !info) return fail(NET_EINVAL); + { + std::lock_guard lock(g_dnsLock); + if (g_dnsOverridden) { + *info = g_dnsOverride; + return info->secondaryAddress == 0 ? 1 : 2; + } + } std::array servers{}; const std::size_t count = std::min(NetCtlHostDnsServers_nid_no_patch(servers.data(), servers.size()), servers.size()); std::memcpy(info, servers.data(), sizeof(servers)); @@ -2637,8 +2666,12 @@ int APS5_VABI sceNetInetPtonWithScopeId(std::int32_t family, const char* text, v } int APS5_VABI sceNetGetDns6Info(NetDns6Info* info, std::uint32_t flags) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (!g_net_inited) return fail(NET_ENOTINIT); + if (flags != 0 || !info) return fail(NET_EINVAL); + std::lock_guard lock(g_dnsLock); + *info = g_dns6Override; + if (IsZeroAddress(info->primaryAddress)) return 0; + return IsZeroAddress(info->secondaryAddress) ? 1 : 2; } int APS5_VABI sceNetSetDns6InfoToKernel(const NetDns6Info* info, std::uint32_t flags) { diff --git a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp index d59cfc4304..d5c359429b 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp @@ -38,6 +38,7 @@ bool PipeReadState(int descriptor, std::int64_t* data, bool* eof); bool PipeWriteState(int descriptor, std::int64_t* data, bool* eof, bool* writable); std::int64_t Readv(int descriptor, const Iovec* iov, int iovcnt); std::int64_t Writev(int descriptor, const Iovec* iov, int iovcnt); +bool ReadGuest(void* destination, const void* source, std::size_t bytes); } extern "C" bool GuestSocketIsOpen_nid_no_patch(int descriptor); diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 8df1339f8d..8be83b72a2 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -1899,6 +1899,10 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c } } +bool GuestSockets::ReadGuest(void* destination, const void* source, std::size_t bytes) { + return ::ReadGuest(destination, source, bytes); +} + std::int64_t GuestSockets::Read(int descriptor, void* buffer, std::size_t length) { if (length == 0) return IsOpen(descriptor) ? 0 : Fail(9); return recv_nid_postfix(descriptor, buffer, std::min(length, INT_MAX), 0); diff --git a/core/libs/tests/GuestNetDnsInfo.cpp b/core/libs/tests/GuestNetDnsInfo.cpp new file mode 100644 index 0000000000..2dc49bca3e --- /dev/null +++ b/core/libs/tests/GuestNetDnsInfo.cpp @@ -0,0 +1,139 @@ +#include "prx/libSceNet/include/Net.hpp" +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kErrorBase = static_cast(0x80410100); +constexpr int kEfault = 14; +constexpr int kEinval = 22; +constexpr int kEnotinit = 200; + +bool Failed(int result, int error) { return result == (kErrorBase | error) && *sceNetErrnoLoc() == error; } + +void CheckUninitialized() { + NetDnsInfo info{0x08080808u, 0x01010101u}; + const NetDnsInfo before = info; + NetDns6Info info6{}; + std::memset(&info6, 0x5a, sizeof(info6)); + const NetDns6Info before6 = info6; + for (const std::uint32_t flags : {0u, 1u, 0xffffffffu}) { + Require(Failed(sceNetGetDnsInfo(&info, flags), kEnotinit)); + Require(Failed(sceNetSetDnsInfo(&info, flags), kEnotinit)); + Require(Failed(sceNetGetDns6Info(&info6, flags), kEnotinit)); + Require(Failed(sceNetSetDns6Info(&info6, flags), kEnotinit)); + Require(Failed(sceNetGetDnsInfo(nullptr, flags), kEnotinit)); + Require(Failed(sceNetSetDnsInfo(nullptr, flags), kEnotinit)); + Require(Failed(sceNetGetDns6Info(nullptr, flags), kEnotinit)); + Require(Failed(sceNetSetDns6Info(nullptr, flags), kEnotinit)); + Require(std::memcmp(&info, &before, sizeof(info)) == 0); + Require(std::memcmp(&info6, &before6, sizeof(info6)) == 0); + } +} + +void CheckDns() { + NetDnsInfo info{0x08080808u, 0}; + Require(Failed(sceNetSetDnsInfo(&info, 1), kEinval)); + NetDnsInfo empty{0, 0x01010101u}; + Require(Failed(sceNetSetDnsInfo(&empty, 0), kEinval)); + + Require(sceNetSetDnsInfo(&info, 0) == 0); + NetDnsInfo read{1, 1}; + Require(sceNetGetDnsInfo(&read, 0) == 1 && read.primaryAddress == 0x08080808u && read.secondaryAddress == 0); + info.secondaryAddress = 0x04040808u; + Require(sceNetSetDnsInfo(&info, 0) == 0); + Require(sceNetGetDnsInfo(&read, 0) == 2 && read.primaryAddress == 0x08080808u && read.secondaryAddress == 0x04040808u); + Require(sceNetSetDnsInfo(nullptr, 0) == 0); + Require(sceNetGetDnsInfo(&read, 0) >= 0); + + NetDns6Info info6{}; + Require(Failed(sceNetGetDnsInfo(nullptr, 0), kEinval)); + Require(Failed(sceNetGetDns6Info(nullptr, 0), kEinval)); + for (const std::uint32_t flags : {1u, 2u, 0xffffffffu}) { + NetDnsInfo unchanged{0x5a5a5a5au, 0x5a5a5a5au}; + const NetDnsInfo before = unchanged; + std::memset(&info6, 0x5a, sizeof(info6)); + const NetDns6Info before6 = info6; + Require(Failed(sceNetGetDnsInfo(&unchanged, flags), kEinval)); + Require(Failed(sceNetGetDns6Info(&info6, flags), kEinval)); + Require(Failed(sceNetGetDnsInfo(nullptr, flags), kEinval)); + Require(Failed(sceNetGetDns6Info(nullptr, flags), kEinval)); + Require(Failed(sceNetSetDnsInfo(nullptr, flags), kEinval)); + Require(Failed(sceNetSetDns6Info(nullptr, flags), kEinval)); + Require(std::memcmp(&unchanged, &before, sizeof(before)) == 0); + Require(std::memcmp(&info6, &before6, sizeof(before6)) == 0); + } + std::memset(&info6, 0xff, sizeof(info6)); + Require(sceNetGetDns6Info(&info6, 0) == 0); + for (const auto byte : info6.primaryAddress.bytes) Require(byte == 0); + for (const auto byte : info6.secondaryAddress.bytes) Require(byte == 0); + + NetDns6Info set6{}; + Require(Failed(sceNetSetDns6Info(&set6, 0), kEinval)); + set6.primaryAddress.bytes[0] = 0x20; + set6.primaryAddress.bytes[15] = 0x01; + Require(Failed(sceNetSetDns6Info(&set6, 2), kEinval)); + Require(sceNetSetDns6Info(&set6, 0) == 0); + Require(sceNetGetDns6Info(&info6, 0) == 1 && std::memcmp(&info6, &set6, sizeof(set6)) == 0); + set6.secondaryAddress.bytes[0] = 0x26; + Require(sceNetSetDns6Info(&set6, 0) == 0); + Require(sceNetGetDns6Info(&info6, 0) == 2 && std::memcmp(&info6, &set6, sizeof(set6)) == 0); + Require(sceNetSetDns6Info(nullptr, 0) == 0); + Require(sceNetGetDns6Info(&info6, 0) == 0); +} + +} + +void* UnreadablePage() { +#ifdef _WIN32 + return VirtualAlloc(nullptr, 4096, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS); +#else + void* page = mmap(nullptr, 4096, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + return page == MAP_FAILED ? nullptr : page; +#endif +} + +void CheckUnreadableInfo() { + void* page = UnreadablePage(); + Require(page != nullptr); + NetDnsInfo info{0x08080808u, 0}; + Require(sceNetSetDnsInfo(&info, 0) == 0); + Require(Failed(sceNetSetDnsInfo(static_cast(page), 1), kEinval)); + Require(Failed(sceNetSetDnsInfo(static_cast(page), 0), kEfault)); + NetDnsInfo read{}; + Require(sceNetGetDnsInfo(&read, 0) == 1 && read.primaryAddress == 0x08080808u); + Require(sceNetSetDnsInfo(nullptr, 0) == 0); + + NetDns6Info info6{}; + info6.primaryAddress.bytes[0] = 0x20; + Require(sceNetSetDns6Info(&info6, 0) == 0); + Require(Failed(sceNetSetDns6Info(static_cast(page), 1), kEinval)); + Require(Failed(sceNetSetDns6Info(static_cast(page), 0), kEfault)); + NetDns6Info read6{}; + Require(sceNetGetDns6Info(&read6, 0) == 1 && std::memcmp(&read6, &info6, sizeof(info6)) == 0); + Require(sceNetSetDns6Info(nullptr, 0) == 0); +#ifdef _WIN32 + Require(VirtualFree(page, 0, MEM_RELEASE) != 0); +#else + Require(munmap(page, 4096) == 0); +#endif +} + +int main() { + CheckUninitialized(); + Require(sceNetInit_nid_postfix() == 0); + CheckDns(); + CheckUnreadableInfo(); + Require(sceNetTerm() == 0); + CheckUninitialized(); + return 0; +} diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 9604c366a3..67ef1daf4a 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -2329,13 +2329,11 @@ int main() { Require(std::strcmp(text, reinterpret_cast(info.opaque)) == 0); } for (const int flags : {0x1, -1}) { - bool threw = false; - try { sceNetGetDnsInfo(reinterpret_cast(dns.data()), flags); } catch (const std::runtime_error&) { threw = true; } - Require(threw); + const auto before = dns; + Require(Failed(sceNetGetDnsInfo(reinterpret_cast(dns.data()), flags), 22)); + Require(dns == before); } - bool threw = false; - try { sceNetGetDnsInfo(nullptr, 0); } catch (const std::runtime_error&) { threw = true; } - Require(threw); + Require(Failed(sceNetGetDnsInfo(nullptr, 0), 22)); } NetCtlIfStat first{}; first.device = 7; diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 0fd8b2ebca..15ad5fee41 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -18,7 +18,7 @@ - [sceHttp2GetMemoryPoolStats, sceSslGetMemoryPoolStats, sceNetGetMemoryPoolStats](../../../core/libs/prx/libSceNet/Export.cpp) (libSceHttp2, libSceSsl, libSceNet) - the signatures `(int id, stats*)`, the layouts (pool size, peak and current use, and for Http2 and Ssl a 32-bit reserved field) and the error codes were measured on a PS5 (firmware 13.60). The pool size is the one given at init or pool creation. Http2 and Net allocate nothing from these pools. Ssl counts its certificate backend allocations and returned data against its pool; C++ bookkeeping is outside that budget. All three initially report zero use, where the console already reports 0x15C0 bytes for Ssl, 0x1640 for Http2 and 4 for a Net pool. Ssl's allocation sizes and peak usage do not reproduce the console allocator - [sceNetClearDnsCache](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the library has no DNS cache to clear because lookups use the host resolver. The host resolver cache is left unchanged. - [sceNetEtherStrton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - strings shorter than 17 bytes return EINVAL without changing the output, rather than reading past the terminating NUL as in the [firmware 13.60 probe](https://gist.github.com/orezraey/789290577123af3c45b340d6db6f395c/71e0a6215c7ee86f49fdb31de989dd2b50720b7a). Non-ASCII digit bytes throw because their decoding is unmeasured; other firmware versions remain unverified. Fixed character pairs, ignored separators, alphabetic values beyond f, non-alphanumeric ASCII values of 0x7f, and null-pointer EINVAL follow those measurements, not the strict [shadPS4 parser](https://github.com/shadps4-emu/shadPS4/blob/d615b3887ba29025a59242e1a26f5b68bb176158/src/core/libraries/net/net.cpp#L1274-L1297) -- [sceNetGetDnsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(info, int flags)` signature, the two IPv4 server slots in network byte order and the server count as the return value follow the PS4 [OpenOrbis musl resolver](https://github.com/OpenOrbis/musl/blob/2b0bb8ce2fa64ac255cb656785729d159a5f074c/src/network/resolvconf.c) and [OpenOrbis Net.h](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/Net.h); unverified on PS5 hardware. It reports the host DNS servers of the interface `sceNetCtlGetInfo` reports, never servers set through `sceNetSetDnsInfo`. Zeroing the unused slots and returning 0 without a host IPv4 interface are unverified. Nonzero flags and a null info throw because their console results are unknown +- [sceNetGetDnsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - host-interface selection, zeroing unused slots and returning 0 without a host IPv4 interface remain unverified on PS5 hardware. It reports the host interface's DNS servers until a guest override is set. The two network-order IPv4 slots and returned count follow the PS4 [OpenOrbis musl resolver](https://github.com/OpenOrbis/musl/blob/2b0bb8ce2fa64ac255cb656785729d159a5f074c/src/network/resolvconf.c) and [Net.h](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/Net.h). - [sceNetGetRandom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - generates values with the host random device; the system library maintains its own seeded PRNG state. - [sceNetInetPtonEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - for IPv6 a null `dst` returns `NET_EINVAL` here, where it crashed a payload process on a PS5 (firmware 13.60). The `(int af, const char* src, void* dst, int flags)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp). On PS4 firmware 10.71 and on that PS5, IPv4 text is read as `inet_aton` reads it (`a`, `a.b`, `a.b.c`, hex and octal parts, values wrapping at 32 bits, the text ending at a space, tab or VT), except that a part starting with `09` is decimal (`"0989"` is 989) and an octal part with `8` or `9` fails; flags were ignored (PS4: 0, single bits up to 0x100, 0x10000, 0x80000000, -1; PS5: 1, 2, 4, 0x100, 0x10000, -1). On the PS5 only: LF, FF and CR also end the text, a null `src` returns `NET_EINVAL`, and a null `dst` returns 1 or 0 without writing or setting errno. IPv6 converts as `sceNetInetPton` does - [sceNetMemoryAllocate and sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - allocations use the guest heap instead of the system network pool. Pool limits, its allocation accounting and free-time fill are not reproduced: a reused block allocated without flag 2 holds 0xdd bytes on the console and its previous contents here. Flag 4 bypasses the system pool reserve threshold; with no pool there is no threshold, so it allocates as other flags do. What the console returns once the pool is exhausted is unmeasured; AnyPS5 returns null without setting an errno. Measured on a PS5 (firmware 13.60): a size of 0 or less returns null and leaves `sceNetErrnoLoc` unchanged, flag 2 zero-fills, flags 1, 4, 8, 0x10, 0x100 and 0x80000000 had no visible effect, and every block of 1 to 65536 bytes was 64-byte aligned @@ -55,3 +55,4 @@ - [sceNetInetPton, sceNetInetNtop](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) convert IPv6 text with libkernel's [inet_pton and inet_ntop](../../../core/libs/prx/libkernel/Socket/src/AddressConversion.cpp), so they take the same FreeBSD 11.4 text forms; beyond the PS5 measurements in the entry above, these are unverified on a console. IPv4 text keeps the host conversion. The checks and errors before the conversion are libSceNet's own and unchanged - [sceNetDhcpStart, sceNetAddrConfig6Start, sceNetDuplicateIpStart and sceNetInfoDumpStart](../../../core/libs/prx/libSceNet/Export.cpp) - argument widths and callback ABI are recovered. Address callbacks receive user data, a 32-bit event and a 32-bit error. DHCP configuration is mutable; the 512-byte IPv6 configuration is read-only. Their complete field layouts remain unknown. The two 32-bit thread parameters are stored but not consumed by this firmware thread wrapper, so their original names and units are unconfirmed. Information-dump options contain flags, an interval in seconds and a 64-byte path. All four exports remain unimplemented. +- [sceNetSetDnsInfo, sceNetSetDns6Info and sceNetGetDns6Info](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the resolver still uses the host's DNS servers; setters only store servers for the getters. Without an IPv6 override, the getter reports none. Counts follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0918faa0255e9816b795b62c9b09eaea51a1bc5e/src/core/libraries/net/net.cpp), not hardware measurements. All four DNS calls' initialization and argument errors match a reported [firmware 13.42 module probe](https://github.com/boykopovar/AnyPS5/pull/9506#issuecomment-6390574180): ENOTINIT first, then EINVAL for nonzero flags, null getter outputs or zero primary servers; null setters clear the override. Setters use a checked guest read and return EFAULT for unreadable non-null inputs, an AnyPS5 safety measure absent from the probed module. From 44393a3350e4ce088d5ab3cdf0f38c129f1bb121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 25 Oct 2026 18:56:25 +0100 Subject: [PATCH 418/428] fix(net): reach the guest read through a _nid_no_patch export Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceNet/Export.cpp | 4 ++-- core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp | 2 +- core/libs/prx/libkernel/Socket/src/Udp.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 1fb564febf..c5192215bd 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -2274,7 +2274,7 @@ int APS5_VABI sceNetSetDnsInfo(const NetDnsInfo* info, std::uint32_t flags) { if (!g_net_inited) return fail(NET_ENOTINIT); if (flags != 0) return fail(NET_EINVAL); NetDnsInfo servers{}; - if (info && !GuestSockets::ReadGuest(&servers, info, sizeof(servers))) return fail(NET_EFAULT); + if (info && !GuestSocketReadGuest_nid_no_patch(&servers, info, sizeof(servers))) return fail(NET_EFAULT); if (info && servers.primaryAddress == 0) return fail(NET_EINVAL); std::lock_guard lock(g_dnsLock); g_dnsOverridden = info != nullptr; @@ -2330,7 +2330,7 @@ int APS5_VABI sceNetSetDns6Info(const NetDns6Info* info, std::uint32_t flags) { if (!g_net_inited) return fail(NET_ENOTINIT); if (flags != 0) return fail(NET_EINVAL); NetDns6Info servers{}; - if (info && !GuestSockets::ReadGuest(&servers, info, sizeof(servers))) return fail(NET_EFAULT); + if (info && !GuestSocketReadGuest_nid_no_patch(&servers, info, sizeof(servers))) return fail(NET_EFAULT); if (info && IsZeroAddress(servers.primaryAddress)) return fail(NET_EINVAL); std::lock_guard lock(g_dnsLock); g_dns6Override = servers; diff --git a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp index d5c359429b..c5343daede 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp @@ -38,7 +38,7 @@ bool PipeReadState(int descriptor, std::int64_t* data, bool* eof); bool PipeWriteState(int descriptor, std::int64_t* data, bool* eof, bool* writable); std::int64_t Readv(int descriptor, const Iovec* iov, int iovcnt); std::int64_t Writev(int descriptor, const Iovec* iov, int iovcnt); -bool ReadGuest(void* destination, const void* source, std::size_t bytes); } extern "C" bool GuestSocketIsOpen_nid_no_patch(int descriptor); +extern "C" bool GuestSocketReadGuest_nid_no_patch(void* destination, const void* source, std::size_t bytes); diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 8be83b72a2..a9047baa95 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -1899,7 +1899,7 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c } } -bool GuestSockets::ReadGuest(void* destination, const void* source, std::size_t bytes) { +extern "C" bool GuestSocketReadGuest_nid_no_patch(void* destination, const void* source, std::size_t bytes) { return ::ReadGuest(destination, source, bytes); } From 7c36d37671925e9a741f9c77b1af68fbbd8ff2ea Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:36:22 -0700 Subject: [PATCH 419/428] fix(libkernel): clear POLLOUT from a hung-up socket in poll on Linux FreeBSD's pollscan clears POLLOUT whenever POLLHUP is set, for every descriptor. 64ca6e332 applied that to pipes, but a stream socket whose peer has closed and whose own sending side is shut down still reported POLLOUT next to POLLHUP, because Linux's poll sets both. Linux sets POLLHUP only once both directions are shut, the same condition as FreeBSD, so poll now clears POLLOUT from any entry that reports POLLHUP there. --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 3 +++ core/libs/tests/GuestPoll.cpp | 27 ++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 8df1339f8d..7b81dd7429 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -1834,6 +1834,9 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c if (pipe && (entry.revents & GuestPollError)) entry.revents = static_cast((entry.revents & ~GuestPollError) | GuestPollHangUp); if (pipe && (entry.revents & GuestPollHangUp)) entry.revents = static_cast((entry.revents & ~GuestPollWritable) | (entry.events & GuestPollReadable)); +#ifndef _WIN32 + if (entry.revents & GuestPollHangUp) entry.revents = static_cast(entry.revents & ~GuestPollWritable); +#endif if (entry.revents) ++ready; }; const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout < 0 ? 0 : timeout); diff --git a/core/libs/tests/GuestPoll.cpp b/core/libs/tests/GuestPoll.cpp index 8d9c7ee484..c7d86b21db 100644 --- a/core/libs/tests/GuestPoll.cpp +++ b/core/libs/tests/GuestPoll.cpp @@ -261,9 +261,36 @@ static void PollsAStreamWhosePeerClosed() { for (const int descriptor : {client, server, listener}) Require(close_nid_postfix(descriptor) == 0); } +#ifndef _WIN32 +static void PollsAHungUpSocketWithoutPollout() { + const int listener = socket_nid_postfix(2, 1, 0); + Require(listener >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &size) == 0 && listen_nid_postfix(listener, 1) == 0); + const int client = socket_nid_postfix(2, 1, 0); + Require(client >= 0 && connect_nid_postfix(client, address.data(), address.size()) == 0); + const int server = accept_nid_postfix(listener, nullptr, nullptr); + Require(server >= 0); + Require(close_nid_postfix(client) == 0); + char byte = 0; + GuestPollDescriptor ended{server, In, 0}; + Require(poll_nid_postfix(&ended, 1, 1000) == 1); + Require(recv_nid_postfix(server, &byte, 1, 0) == 0); + Require(shutdown_nid_postfix(server, 1) == 0); + GuestPollDescriptor hungUp{server, static_cast(In | Out), 0}; + Require(poll_nid_postfix(&hungUp, 1, 1000) == 1 && hungUp.revents == (In | Hup)); + Require(close_nid_postfix(server) == 0 && close_nid_postfix(listener) == 0); +} +#endif + int main() { PollsUrgentData(); PollsAStreamWhosePeerClosed(); +#ifndef _WIN32 + PollsAHungUpSocketWithoutPollout(); +#endif const int listener = socket_nid_postfix(2, 1, 0); Require(listener >= 0 && listener < 1024); std::array address{16, 2, 0, 0, 127, 0, 0, 1}; From d0c945ce8678c22b8c2fa2c2f28604b8b4eb21c0 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:36:23 -0700 Subject: [PATCH 420/428] docs(libkernel): record how poll reports a hung-up stream socket on each host --- docs/dev/debt/libkernel.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index aa6fbe5269..2f662f288e 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -229,6 +229,7 @@ - [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number - [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A kqueue is ready for `POLLIN` and `POLLRDNORM` while one of its events is triggered and for no other event, as FreeBSD 11.4's [`kqueue_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c#L1900-L1924) answers. A kqueue cannot be waited on either, so on every host `poll` checks it every millisecond and waits on the other descriptors in slices of 1 ms. A kqueue closed during the wait reports `POLLNVAL`, and one whose number was reused is polled again with the rest of the timeout, as FreeBSD 11.4's [`pollrescan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1499-L1519) looks the descriptor up again. A close that has already removed the kqueue but not yet closed its host descriptor is seen as a descriptor of unknown kind, which throws. A regular file, a directory or a file read from app0.zar is always ready for `POLLIN`, `POLLRDNORM` and `POLLOUT` and never for `POLLPRI`, `POLLRDBAND` or `POLLWRBAND`, as FreeBSD 11.4's [`vop_stdpoll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_default.c#L563-L574) and `vop_nopoll` answer every event `poll` accepts; which of the two the console's filesystems use is not known, and they agree on these events. `/dev/random` and `/dev/urandom` follow FreeBSD 11.4's [`randomdev_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/dev/random/randomdev.c#L274-L285) for a seeded device: a request with `POLLIN` or `POLLRDNORM` gets only those bits back, and any other request gets every requested bit. Open descriptors 0 to 2 follow the console model `select` uses: descriptor 0 never reports an event and descriptors 1 and 2 report `POLLOUT`, whatever the host attaches to them. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket, a pipe, a file, a directory, the random device nor descriptor 0 to 2 (other devices) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket, pipe or kqueue to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked - [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) on Windows reports a guest stream socket whose peer closed as readable and adds `POLLHUP` only once the socket has shut its own sending side, as FreeBSD 11.4's [`sopoll_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3080-L3086) does; `WSAPoll` reports `POLLHUP` alone in both cases. A reset or refused connection (`POLLHUP` with `POLLERR`) keeps `WSAPoll`'s flags, a libSceNet socket keeps them too, and a call that asks for no event still returns early on the peer's close with nothing reported. Not measured on a console. +- [poll on a stream socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - on Linux a socket that reports `POLLHUP` reports it without `POLLOUT`, as FreeBSD 11.4's `pollscan` clears `POLLOUT` whenever it reports `POLLHUP`; Linux, like FreeBSD, reports `POLLHUP` only once both of the socket's directions are shut. Windows keeps the flags `WSAPoll` reports. Not measured on a console - [kevent EV_CLEAR on a socket or a pipe](../../../core/libs/prx/libkernel/Equeue/src/EdgeWatch.cpp) (libkernel) - FreeBSD 11 activates an `EV_CLEAR` knote when `kqueue_register` finds it ready, which every `EV_ADD` or `EV_ENABLE` of an enabled knote does again, and on each `KNOTE` the socket or pipe raises (`sorwakeup`, `sowwakeup`, `pipeselwakeup`); [`kqueue_scan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c) checks the filter again before reporting, drops a knote that is no longer ready and deactivates a reported one until the next activation. On Linux each such knote registers the host socket or pipe with its own epoll instance in edge-triggered mode (`EPOLLIN | EPOLLRDHUP` for `EVFILT_READ`, `EPOLLOUT` for `EVFILT_WRITE`), and a wakeup of the host descriptor with that mask stands for a `KNOTE`: the knote is then checked with the same readiness test as a level-triggered one, `NOTE_LOWAT` mark included, and a wait blocks on the epoll descriptor instead of the socket, so a socket that stays readable does not wake it. The host's wakeups are not FreeBSD's: FreeBSD raises `sowwakeup` whenever send buffer space is freed, while Linux TCP wakes `EPOLLOUT` waiters only once the socket is writable again after a write found it full (`SOCK_NOSPACE`, [`tcp_check_space`](https://github.com/torvalds/linux/blob/v6.8/net/ipv4/tcp_input.c#L5639)), so a TCP writer that never fills its buffer sees fewer `EVFILT_WRITE` events than on FreeBSD; a host stack may also coalesce data wakeups, so the number of times a busy socket fires can differ. Windows and macOS throw, as the host there has no edge-triggered wait over these descriptors that is wired up; `EV_CLEAR` on the timer, user and vnode filters does not use this and works everywhere. - [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - host pipes are one-way, where FreeBSD's [`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422) opens both ends for reading and writing, so `EVFILT_READ` on the write end and `EVFILT_WRITE` on the read end throw; on macOS `EVFILT_WRITE` on a pipe throws, as the host has no call for a pipe's capacity. `EVFILT_WRITE` reports the free space of the host buffer, not of FreeBSD's 16 KiB one, and on Linux fires when the host says the pipe is writable rather than at `PIPE_BUF` (512) free bytes. Windows pipes cannot be waited on, so a kqueue waiting on one checks it every 1 ms. Otherwise [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1828) and `filt_pipewrite` are followed: `EV_EOF` once the other end closes, `EPIPE` for a write event added after the read end closed, and `NOTE_LOWAT` ignored. On Windows `fcntl` cannot make a pipe write end non-blocking, so the full-pipe and drain case of `EVFILT_WRITE` is tested on Linux only and the `NtQueryInformationFile` write quota is checked there only for the free space, `EV_EOF` and `EPIPE`. - [kevent on a kqueue](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on a kqueue counts the watched queue's enabled notes that are ready when it is evaluated, where [`filt_kqueue`](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/sys/kern/kern_event.c#L363-L370) reports `kq_count`, the queued notes: a queued note counts until that kqueue is scanned, even once disabled or no longer ready (a socket or pipe read empty, a user note cleared, a watched kqueue drained), so FreeBSD can report it readable where this reports nothing. A queued `EV_ONESHOT` note is reported there with its last count and deleted; here it is evaluated again and, if the watched queue is empty by then, neither reported nor deleted. `EV_CLEAR` throws, as FreeBSD re-arms it only when the watched kqueue queues a note. A cycle of watches throws; a note on the queue itself is `EINVAL`. Not measured on a console From b50ed5995315c2d84114c2775f13d106cff1d1b3 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:45:04 +0200 Subject: [PATCH 421/428] fix(libSceFreeType): preserve referenced face lifetimes --- core/libs/prx/libSceFreeType/include/Api.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceFreeType/include/Api.hpp b/core/libs/prx/libSceFreeType/include/Api.hpp index edf90a2637..0c08f7b10f 100644 --- a/core/libs/prx/libSceFreeType/include/Api.hpp +++ b/core/libs/prx/libSceFreeType/include/Api.hpp @@ -12,6 +12,7 @@ int APS5_VABI FT_New_Face_nid_postfix(FreeType::GuestLibrary* library, const cha int APS5_VABI FT_New_Memory_Face_nid_postfix(FreeType::GuestLibrary* library, const unsigned char* fileBase, std::int64_t fileSize, std::int64_t faceIndex, FreeType::GuestFaceRec** aface); int APS5_VABI FT_Reference_Face_nid_postfix(FreeType::GuestFaceRec* face); int APS5_VABI FT_Done_Face_nid_postfix(FreeType::GuestFaceRec* face); +int APS5_VABI FT_Reference_Face_nid_postfix(FreeType::GuestFaceRec* face); int APS5_VABI FT_Select_Size_nid_postfix(FreeType::GuestFaceRec* face, std::int32_t strikeIndex); int APS5_VABI FT_Set_Pixel_Sizes_nid_postfix(FreeType::GuestFaceRec* face, std::uint32_t pixelWidth, std::uint32_t pixelHeight); int APS5_VABI FT_Set_Char_Size_nid_postfix(FreeType::GuestFaceRec* face, std::int64_t charWidth, std::int64_t charHeight, std::uint32_t horizontalResolution, std::uint32_t verticalResolution); From a476e421882377b61be8d9ae972c30c218e74804 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Thu, 22 Oct 2026 03:51:33 +0200 Subject: [PATCH 422/428] feat(libSceFreeType): manage multiple size objects --- core/libs/prx/libSceFreeType/Export.cpp | 66 +++++- core/libs/prx/libSceFreeType/include/Api.hpp | 3 + .../libSceFreeType/include/GuestObjects.hpp | 10 + .../prx/libSceFreeType/src/GuestObjects.cpp | 43 +++- core/libs/tests/GuestFreeType.cpp | 206 ++++++++++++++++++ docs/dev/debt/libSceFreeType.md | 2 + 6 files changed, 319 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceFreeType/Export.cpp b/core/libs/prx/libSceFreeType/Export.cpp index bb29de9253..a8919bfb49 100644 --- a/core/libs/prx/libSceFreeType/Export.cpp +++ b/core/libs/prx/libSceFreeType/Export.cpp @@ -6,6 +6,7 @@ #include FT_GASP_H #include FT_CID_H +#include FT_SIZES_H #include #include @@ -33,14 +34,17 @@ void FinalizeSlot(void* object) { void FinalizeSize(void* object) { auto* host = static_cast(object); - auto* face = static_cast(host->generic.data); - if (face->size.generic.finalizer) face->size.generic.finalizer(&face->size); - face->rec.size = nullptr; + auto* rec = static_cast(host->generic.data); + auto* face = static_cast(host->face->generic.data); + SyncSizeRecord_nid_no_patch(*face, *rec, host); + SyncSizePointer_nid_no_patch(*face); + if (rec->generic.finalizer) rec->generic.finalizer(rec); } void FinalizeFace(void* object) { auto* host = static_cast(object); auto* face = static_cast(host->generic.data); + SyncSizePointer_nid_no_patch(*face); if (face->rec.generic.finalizer) face->rec.generic.finalizer(&face->rec); face->finalized = true; } @@ -54,7 +58,7 @@ void PublishFace(GuestLibrary* library, std::unique_ptr face, FT_Face library->faces.push_back(std::move(face)); host->generic = {owner, FinalizeFace}; if (host->glyph) host->glyph->generic = {owner, FinalizeSlot}; - if (host->size) host->size->generic = {owner, FinalizeSize}; + if (host->size) host->size->generic = {&owner->size, FinalizeSize}; *aface = &owner->rec; hostOwner.release(); } @@ -164,6 +168,60 @@ int APS5_VABI FT_Done_Face_nid_postfix(GuestFaceRec* face) { return error; } +int APS5_VABI FT_New_Size_nid_postfix(GuestFaceRec* face, GuestSizeRec** asize) { + GuestFace* owner = FaceOf(face); + FT_Face hostFace = owner ? owner->host : nullptr; + if (!hostFace || !asize || !hostFace->driver) { + FT_Size unused = nullptr; + return FT_New_Size(hostFace, asize ? &unused : nullptr); + } + const bool activates = asize == &owner->rec.size; + *asize = nullptr; + if (activates) hostFace->size = nullptr; + try { + owner->sizes.emplace_back(); + } catch (const std::bad_alloc&) { + return FT_Err_Out_Of_Memory; + } + auto entry = std::prev(owner->sizes.end()); + FT_Size host = nullptr; + const FT_Error error = FT_New_Size(hostFace, activates ? &hostFace->size : &host); + if (activates) host = hostFace->size; + if (error != 0) { + owner->sizes.erase(entry); + SyncSizePointer_nid_no_patch(*owner); + return error; + } + entry->host = host; + SyncSizeRecord_nid_no_patch(*owner, entry->rec, host); + host->generic = {&entry->rec, FinalizeSize}; + *asize = &entry->rec; + SyncSizePointer_nid_no_patch(*owner); + return 0; +} + +int APS5_VABI FT_Activate_Size_nid_postfix(GuestSizeRec* size) { + FT_Size host = size ? static_cast(size->internal) : nullptr; + const FT_Error error = FT_Activate_Size(host); + if (error == 0) SyncSize(*static_cast(host->face->generic.data)); + return error; +} + +int APS5_VABI FT_Done_Size_nid_postfix(GuestSizeRec* size) { + FT_Size host = size ? static_cast(size->internal) : nullptr; + auto* owner = host && host->face ? static_cast(host->face->generic.data) : nullptr; + const FT_Error error = FT_Done_Size(host); + if (error != 0) return error; + if (host == owner->initialSize) { + owner->initialSize = nullptr; + owner->size.internal = nullptr; + } else { + owner->sizes.remove_if([host](const auto& entry) { return entry.host == host; }); + } + SyncSize(*owner); + return 0; +} + int APS5_VABI FT_Set_Pixel_Sizes_nid_postfix(GuestFaceRec* face, std::uint32_t pixel_width, std::uint32_t pixel_height) { GuestFace* owner = FaceOf(face); const FT_Error error = FT_Set_Pixel_Sizes(owner ? owner->host : nullptr, pixel_width, pixel_height); diff --git a/core/libs/prx/libSceFreeType/include/Api.hpp b/core/libs/prx/libSceFreeType/include/Api.hpp index 0c08f7b10f..6b7e44831e 100644 --- a/core/libs/prx/libSceFreeType/include/Api.hpp +++ b/core/libs/prx/libSceFreeType/include/Api.hpp @@ -14,6 +14,9 @@ int APS5_VABI FT_Reference_Face_nid_postfix(FreeType::GuestFaceRec* face); int APS5_VABI FT_Done_Face_nid_postfix(FreeType::GuestFaceRec* face); int APS5_VABI FT_Reference_Face_nid_postfix(FreeType::GuestFaceRec* face); int APS5_VABI FT_Select_Size_nid_postfix(FreeType::GuestFaceRec* face, std::int32_t strikeIndex); +int APS5_VABI FT_New_Size_nid_postfix(FreeType::GuestFaceRec* face, FreeType::GuestSizeRec** asize); +int APS5_VABI FT_Activate_Size_nid_postfix(FreeType::GuestSizeRec* size); +int APS5_VABI FT_Done_Size_nid_postfix(FreeType::GuestSizeRec* size); int APS5_VABI FT_Set_Pixel_Sizes_nid_postfix(FreeType::GuestFaceRec* face, std::uint32_t pixelWidth, std::uint32_t pixelHeight); int APS5_VABI FT_Set_Char_Size_nid_postfix(FreeType::GuestFaceRec* face, std::int64_t charWidth, std::int64_t charHeight, std::uint32_t horizontalResolution, std::uint32_t verticalResolution); std::uint32_t APS5_VABI FT_Get_Char_Index_nid_postfix(FreeType::GuestFaceRec* face, std::uint64_t charcode); diff --git a/core/libs/prx/libSceFreeType/include/GuestObjects.hpp b/core/libs/prx/libSceFreeType/include/GuestObjects.hpp index 081d70665f..d47c3c1037 100644 --- a/core/libs/prx/libSceFreeType/include/GuestObjects.hpp +++ b/core/libs/prx/libSceFreeType/include/GuestObjects.hpp @@ -21,6 +21,11 @@ struct GuestLibrary { void UnregisterBitmap_nid_no_patch(const GuestBitmap* bitmap); +struct GuestSize { + GuestSizeRec rec{}; + FT_Size host = nullptr; +}; + struct GuestFace { GuestFaceRec rec{}; GuestGlyphSlotRec slot{}; @@ -28,6 +33,9 @@ struct GuestFace { GuestLibrary* library = nullptr; FT_Face host = nullptr; bool finalized = false; + FT_Size initialSize = nullptr; + bool sizeInitialized = false; + std::list sizes; std::vector bytes; std::vector fixedSizes; std::vector charmaps; @@ -43,6 +51,8 @@ GuestFace* FaceOf(const GuestFaceRec* face); GuestFace* FaceOf(const GuestGlyphSlotRec* slot); void SyncFace(GuestFace& face); void SyncSize(GuestFace& face); +void SyncSizePointer_nid_no_patch(GuestFace& face); +void SyncSizeRecord_nid_no_patch(GuestFace& face, GuestSizeRec& rec, FT_Size host); void SyncSlot(GuestFace& face); } diff --git a/core/libs/prx/libSceFreeType/src/GuestObjects.cpp b/core/libs/prx/libSceFreeType/src/GuestObjects.cpp index 17d1888a21..1ecb345092 100644 --- a/core/libs/prx/libSceFreeType/src/GuestObjects.cpp +++ b/core/libs/prx/libSceFreeType/src/GuestObjects.cpp @@ -1,6 +1,7 @@ #include "prx/libSceFreeType/include/GuestObjects.hpp" #include +#include namespace FreeType { @@ -51,20 +52,48 @@ void SyncFace(GuestFace& face) { rec.underline_position = host->underline_position; rec.underline_thickness = host->underline_thickness; rec.glyph = host->glyph ? &face.slot : nullptr; - rec.size = host->size ? &face.size : nullptr; rec.internal = &face; SyncSize(face); SyncSlot(face); } -void SyncSize(GuestFace& face) { - face.size.face = &face.rec; - if (!face.host->size) { - face.size.metrics = {}; +void SyncSizeRecord_nid_no_patch(GuestFace& face, GuestSizeRec& rec, FT_Size host) { + rec.face = &face.rec; + rec.internal = host; + if (!host) { + rec.metrics = {}; return; } - const auto& metrics = face.host->size->metrics; - face.size.metrics = {metrics.x_ppem, metrics.y_ppem, metrics.x_scale, metrics.y_scale, metrics.ascender, metrics.descender, metrics.height, metrics.max_advance}; + const auto& metrics = host->metrics; + rec.metrics = {metrics.x_ppem, metrics.y_ppem, metrics.x_scale, metrics.y_scale, metrics.ascender, metrics.descender, metrics.height, metrics.max_advance}; +} + +void SyncSizePointer_nid_no_patch(GuestFace& face) { + const FT_Size active = face.host->size; + if (!active) { + face.rec.size = nullptr; + return; + } + if (active == face.initialSize) { + face.rec.size = &face.size; + return; + } + for (auto& size : face.sizes) { + if (size.host != active) continue; + face.rec.size = &size.rec; + return; + } + throw std::runtime_error("FreeType: an active size created outside the guest bridge is not implemented"); +} + +void SyncSize(GuestFace& face) { + if (!face.sizeInitialized) { + face.initialSize = face.host->size; + face.sizeInitialized = true; + SyncSizeRecord_nid_no_patch(face, face.size, face.initialSize); + } + SyncSizePointer_nid_no_patch(face); + if (face.rec.size) SyncSizeRecord_nid_no_patch(face, *face.rec.size, face.host->size); } void SyncSlot(GuestFace& face) { diff --git a/core/libs/tests/GuestFreeType.cpp b/core/libs/tests/GuestFreeType.cpp index 86df877d10..15065a9bf6 100644 --- a/core/libs/tests/GuestFreeType.cpp +++ b/core/libs/tests/GuestFreeType.cpp @@ -9,6 +9,8 @@ #include FT_OUTLINE_H #include FT_TRIGONOMETRY_H #include FT_CID_H +#include FT_SIZES_H +#include FT_SYSTEM_H #include #include @@ -1257,7 +1259,211 @@ static void CheckTransforms(const std::vector& font) { std::printf("FT_Set_Transform: %zu native glyph and bitmap comparisons\n", comparisons); } +static void SizeMatches(const GuestSizeRec* size, FT_Size native) { + Require(size != nullptr && native != nullptr); + const auto& a = size->metrics; + const auto& b = native->metrics; + Require(a.x_ppem == b.x_ppem && a.y_ppem == b.y_ppem && a.x_scale == b.x_scale && a.y_scale == b.y_scale); + Require(a.ascender == b.ascender && a.descender == b.descender && a.height == b.height && a.max_advance == b.max_advance); +} + +struct SizeEvents { + std::vector sizes; + std::vector> calls; + + int Index(void* size) const { + if (!size) return -1; + for (std::size_t index = 0; index < sizes.size(); ++index) { + if (sizes[index] == size) return static_cast(index); + } + Require(false); + return -2; + } +}; + +static void NativeMultipleSizeFinalizer(void* object) { + auto* size = static_cast(object); + auto& events = *static_cast(size->generic.data); + events.calls.push_back({events.Index(size), events.Index(size->face->size), size->metrics.y_ppem, size->face->glyph == nullptr}); +} + +static void APS5_VABI GuestMultipleSizeFinalizer(void* object) { + auto* size = static_cast(object); + auto* face = static_cast(size->face); + auto& events = *static_cast(size->generic.data); + events.calls.push_back({events.Index(size), events.Index(face->size), size->metrics.y_ppem, face->glyph == nullptr}); +} + +static void NativeMultipleSizeFaceFinalizer(void* object) { + auto* face = static_cast(object); + Require(face->glyph == nullptr && face->size == nullptr); + static_cast(face->generic.data)->calls.push_back({100, -1, 0, 1}); +} + +static void APS5_VABI GuestMultipleSizeFaceFinalizer(void* object) { + auto* face = static_cast(object); + Require(face->glyph == nullptr && face->size == nullptr); + static_cast(face->generic.data)->calls.push_back({100, -1, 0, 1}); +} + +static FT_Alloc_Func SizeOriginalAlloc = nullptr; +static int SizeAllocationsBeforeFailure = 0; + +static void* SizeFailingAlloc(FT_Memory memory, long bytes) { + if (SizeAllocationsBeforeFailure-- == 0) return nullptr; + return SizeOriginalAlloc(memory, bytes); +} + +static void SizeObjects(const std::vector& font) { + Require(FT_New_Size_nid_postfix(nullptr, nullptr) == FT_New_Size(nullptr, nullptr)); + Require(FT_Done_Size_nid_postfix(nullptr) == FT_Done_Size(nullptr)); + Require(FT_Activate_Size_nid_postfix(nullptr) == FT_Activate_Size(nullptr)); + GuestSizeRec sentinel{}; + GuestSizeRec* untouched = &sentinel; + Require(FT_New_Size_nid_postfix(nullptr, &untouched) == FT_Err_Invalid_Face_Handle && untouched == &sentinel); + FT_FaceRec driverless{}; + GuestFace invalidOwner; + invalidOwner.host = &driverless; + invalidOwner.rec.internal = &invalidOwner; + FT_Size nativeUntouched = reinterpret_cast(&sentinel); + Require(FT_New_Size(&driverless, &nativeUntouched) == FT_Err_Invalid_Driver_Handle); + Require(FT_New_Size_nid_postfix(&invalidOwner.rec, &untouched) == FT_Err_Invalid_Driver_Handle && untouched == &sentinel); + Require(FT_New_Size_nid_postfix(&invalidOwner.rec, nullptr) == FT_New_Size(&driverless, nullptr)); + + FT_SizeRec driverlessSize{}; + GuestSizeRec invalidSize{}; + invalidSize.internal = &driverlessSize; + Require(FT_Done_Size_nid_postfix(&invalidSize) == FT_Done_Size(&driverlessSize)); + Require(FT_Activate_Size_nid_postfix(&invalidSize) == FT_Activate_Size(&driverlessSize)); + driverlessSize.face = &driverless; + Require(FT_Done_Size_nid_postfix(&invalidSize) == FT_Done_Size(&driverlessSize)); + Require(FT_Activate_Size_nid_postfix(&invalidSize) == FT_Activate_Size(&driverlessSize)); + static_assert(sizeof(GuestSizeMetrics) == 56 && sizeof(GuestSizeRec) == 88); + + int scenarios = 0; + int rendered = 0; + int failures = 0; + for (const int faceIndex : {0, -1}) { + for (const int count : {0, 1, 5, 41}) { + for (const int cleanup : {0, 1, 2}) { + void* library = nullptr; + FT_Library oracle = nullptr; + Require(FT_Init_FreeType_nid_postfix(&library) == 0 && FT_Init_FreeType(&oracle) == 0); + GuestFaceRec* face = nullptr; + FT_Face native = nullptr; + Require(FT_New_Memory_Face_nid_postfix(library, font.data(), font.size(), faceIndex, &face) == 0); + Require(FT_New_Memory_Face(oracle, font.data(), static_cast(font.size()), faceIndex, &native) == 0); + SizeEvents actual; + SizeEvents expected; + face->generic = {&actual, GuestMultipleSizeFaceFinalizer}; + native->generic = {&expected, NativeMultipleSizeFaceFinalizer}; + std::vector sizes; + std::vector originals; + auto remember = [&](GuestSizeRec* size, FT_Size original) { + Require(size->face == face && original->face == native); + actual.sizes.push_back(size); + expected.sizes.push_back(original); + size->generic = {&actual, GuestMultipleSizeFinalizer}; + original->generic = {&expected, NativeMultipleSizeFinalizer}; + sizes.push_back(size); + originals.push_back(original); + SizeMatches(size, original); + }; + if (native->size) remember(face->size, native->size); + Require(FT_New_Size_nid_postfix(face, nullptr) == FT_New_Size(native, nullptr)); + for (int index = 0; index < count; ++index) { + GuestSizeRec* before = face->size; + FT_Size previous = native->size; + GuestSizeRec* created = nullptr; + FT_Size original = nullptr; + Require(FT_New_Size_nid_postfix(face, &created) == FT_New_Size(native, &original)); + Require(created != nullptr && original != nullptr && face->size == before && native->size == previous); + remember(created, original); + } + if (count == 5) { + Require(FT_New_Size_nid_postfix(face, &face->size) == FT_New_Size(native, &native->size)); + remember(face->size, native->size); + } + for (std::size_t index = 0; index < sizes.size(); ++index) { + Require(FT_Activate_Size_nid_postfix(sizes[index]) == 0 && FT_Activate_Size(originals[index]) == 0); + Require(face->size == sizes[index] && native->size == originals[index]); + Require(FT_Set_Pixel_Sizes_nid_postfix(face, 0, 12 + index) == FT_Set_Pixel_Sizes(native, 0, 12 + index)); + SizeMatches(sizes[index], originals[index]); + } + for (std::size_t turn = 0; turn < sizes.size() * 3; ++turn) { + const std::size_t index = (turn * 17 + 3) % sizes.size(); + Require(FT_Activate_Size_nid_postfix(sizes[index]) == FT_Activate_Size(originals[index])); + Require(face->size == sizes[index]); + for (std::size_t other = 0; other < sizes.size(); ++other) SizeMatches(sizes[other], originals[other]); + if (!native->glyph) continue; + Require(FT_Load_Char_nid_postfix(face, 'A', FT_LOAD_NO_HINTING) == FT_Load_Char(native, 'A', FT_LOAD_NO_HINTING)); + for (unsigned point = 0; point < face->glyph->outline.n_points; ++point) face->glyph->outline.points[point].x += 64; + FT_Outline_Translate(&native->glyph->outline, 64, 0); + Require(FT_Activate_Size_nid_postfix(sizes[index]) == FT_Activate_Size(originals[index])); + Require(FT_Render_Glyph_nid_postfix(face->glyph, FT_RENDER_MODE_NORMAL) == FT_Render_Glyph(native->glyph, FT_RENDER_MODE_NORMAL)); + SlotMatches(face->glyph, native->glyph); + ++rendered; + } + if (cleanup == 0) { + while (!sizes.empty()) { + const std::size_t index = sizes.size() % 2 == 0 ? 0 : sizes.size() - 1; + const int removed = actual.Index(sizes[index]); + Require(FT_Done_Size_nid_postfix(sizes[index]) == FT_Done_Size(originals[index])); + actual.sizes[removed] = nullptr; + expected.sizes[removed] = nullptr; + sizes.erase(sizes.begin() + index); + originals.erase(originals.begin() + index); + Require(actual.Index(face->size) == expected.Index(native->size) && actual.calls == expected.calls); + } + Require(face->size == nullptr && native->size == nullptr); + Require(FT_Set_Pixel_Sizes_nid_postfix(face, 0, 24) == FT_Set_Pixel_Sizes(native, 0, 24)); + Require(FT_New_Size_nid_postfix(face, &face->size) == FT_New_Size(native, &native->size)); + Require(face->size != nullptr && native->size != nullptr); + remember(face->size, native->size); + Require(FT_Set_Pixel_Sizes_nid_postfix(face, 0, 24) == FT_Set_Pixel_Sizes(native, 0, 24)); + for (const bool alias : {false, true}) { + for (const int allocation : {0, 1, 2}) { + auto* owner = static_cast(face->internal); + FT_Memory memory = owner->host->memory; + SizeOriginalAlloc = memory->alloc; + SizeAllocationsBeforeFailure = allocation; + memory->alloc = SizeFailingAlloc; + GuestSizeRec* output = &sentinel; + const int error = FT_New_Size_nid_postfix(face, alias ? &face->size : &output); + memory->alloc = SizeOriginalAlloc; + Require(error == FT_Err_Out_Of_Memory && owner->sizes.size() == 1); + FT_Memory nativeMemory = native->memory; + SizeOriginalAlloc = nativeMemory->alloc; + SizeAllocationsBeforeFailure = allocation; + nativeMemory->alloc = SizeFailingAlloc; + FT_Size nativeOutput = reinterpret_cast(&sentinel); + const int nativeError = FT_New_Size(native, alias ? &native->size : &nativeOutput); + nativeMemory->alloc = SizeOriginalAlloc; + Require(error == nativeError); + Require(alias ? face->size == nullptr && native->size == nullptr : output == nullptr && nativeOutput == nullptr); + Require(actual.Index(face->size) == expected.Index(native->size)); + Require(actual.calls == expected.calls); + ++failures; + } + } + } + if (cleanup == 1) { + Require(FT_Reference_Face_nid_postfix(face) == 0 && FT_Reference_Face(native) == 0); + Require(FT_Done_Face_nid_postfix(face) == 0 && FT_Done_Face(native) == 0); + Require(actual.calls.empty() && expected.calls.empty()); + } + if (cleanup != 2) Require(FT_Done_Face_nid_postfix(face) == 0 && FT_Done_Face(native) == 0); + Require(FT_Done_FreeType_nid_postfix(library) == 0 && FT_Done_FreeType(oracle) == 0); + Require(actual.calls == expected.calls && !actual.calls.empty() && actual.calls.back()[0] == 100); + ++scenarios; + } + } + } + std::printf("FreeType size objects: %d lifecycles, %d rendered glyphs, %d allocation failures compared with native\n", scenarios, rendered, failures); +} + int main() { + SizeObjects(SquareGlyphFont('A', 700)); { const auto referenceFont = SquareGlyphFont('A', 700); GuestLibrary* referenceLibrary = nullptr; diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index f52d05e5d0..205ee0df69 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -1,6 +1,8 @@ # libSceFreeType technical debt ### Unknown function info +- [FT_New_Size, FT_Activate_Size and FT_Done_Size](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signatures, errors and generic-finalizer ordering are unverified; the LP64 guest adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Private size fields are bridge-owned. Sizes created outside the bridge throw when selected. + - [FT_Reference_Face](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signature, reference-counter limits and finalizer behavior are unverified; the LP64 guest adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Counter overflow and reentrant finalization have not been measured on the console. - [FT_Get_Sfnt_Name_Count, FT_Get_Sfnt_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, record layout and errors are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftsnames.c) and assumes the public LP64 `FT_SfntName` layout (24 bytes, string pointer at 8, byte length at 16). Strings retain their original encoding, borrow native face storage and remain valid only while the face is alive From e0b244d9de4b4e8ea2cc839d20ce6fab422d0939 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Thu, 22 Oct 2026 08:09:55 +0200 Subject: [PATCH 423/428] test(libSceFreeType): pin size ownership through teardown --- core/libs/tests/GuestFreeType.cpp | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestFreeType.cpp b/core/libs/tests/GuestFreeType.cpp index 15065a9bf6..ed1987ef28 100644 --- a/core/libs/tests/GuestFreeType.cpp +++ b/core/libs/tests/GuestFreeType.cpp @@ -1267,6 +1267,14 @@ static void SizeMatches(const GuestSizeRec* size, FT_Size native) { Require(a.ascender == b.ascender && a.descender == b.descender && a.height == b.height && a.max_advance == b.max_advance); } +static bool OwnsSize(const GuestFace& face, const GuestSizeRec* size) { + if (size == &face.size) return true; + for (const auto& entry : face.sizes) { + if (size == &entry.rec) return true; + } + return false; +} + struct SizeEvents { std::vector sizes; std::vector> calls; @@ -1290,6 +1298,10 @@ static void NativeMultipleSizeFinalizer(void* object) { static void APS5_VABI GuestMultipleSizeFinalizer(void* object) { auto* size = static_cast(object); auto* face = static_cast(size->face); + const auto& owner = *static_cast(face->internal); + Require(OwnsSize(owner, size)); + if (face->size) Require(OwnsSize(owner, face->size)); + SizeMatches(size, static_cast(size->internal)); auto& events = *static_cast(size->generic.data); events.calls.push_back({events.Index(size), events.Index(face->size), size->metrics.y_ppem, face->glyph == nullptr}); } @@ -1346,7 +1358,7 @@ static void SizeObjects(const std::vector& font) { for (const int faceIndex : {0, -1}) { for (const int count : {0, 1, 5, 41}) { for (const int cleanup : {0, 1, 2}) { - void* library = nullptr; + GuestLibrary* library = nullptr; FT_Library oracle = nullptr; Require(FT_Init_FreeType_nid_postfix(&library) == 0 && FT_Init_FreeType(&oracle) == 0); GuestFaceRec* face = nullptr; @@ -1452,9 +1464,18 @@ static void SizeObjects(const std::vector& font) { Require(FT_Done_Face_nid_postfix(face) == 0 && FT_Done_Face(native) == 0); Require(actual.calls.empty() && expected.calls.empty()); } + if (cleanup == 2 && count == 5) { + Require(sizes.size() > 2); + Require(FT_Activate_Size_nid_postfix(sizes[1]) == FT_Activate_Size(originals[1])); + Require(face->size == sizes[1] && native->size == originals[1]); + } if (cleanup != 2) Require(FT_Done_Face_nid_postfix(face) == 0 && FT_Done_Face(native) == 0); Require(FT_Done_FreeType_nid_postfix(library) == 0 && FT_Done_FreeType(oracle) == 0); Require(actual.calls == expected.calls && !actual.calls.empty() && actual.calls.back()[0] == 100); + if (cleanup == 2 && count == 5) { + Require(actual.calls.size() > 3 && actual.calls[0][1] == 1 && actual.calls[1][0] == 1); + for (std::size_t index = 2; index + 1 < actual.calls.size(); ++index) Require(actual.calls[index][1] == 1); + } ++scenarios; } } From ad2b0857790f1e1466dee1fee86de94ce201addc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 25 Oct 2026 19:46:07 +0100 Subject: [PATCH 424/428] chore(libSceFreeType): drop duplicate FT_Reference_Face declaration --- core/libs/prx/libSceFreeType/include/Api.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/core/libs/prx/libSceFreeType/include/Api.hpp b/core/libs/prx/libSceFreeType/include/Api.hpp index 6b7e44831e..ac044b7919 100644 --- a/core/libs/prx/libSceFreeType/include/Api.hpp +++ b/core/libs/prx/libSceFreeType/include/Api.hpp @@ -12,7 +12,6 @@ int APS5_VABI FT_New_Face_nid_postfix(FreeType::GuestLibrary* library, const cha int APS5_VABI FT_New_Memory_Face_nid_postfix(FreeType::GuestLibrary* library, const unsigned char* fileBase, std::int64_t fileSize, std::int64_t faceIndex, FreeType::GuestFaceRec** aface); int APS5_VABI FT_Reference_Face_nid_postfix(FreeType::GuestFaceRec* face); int APS5_VABI FT_Done_Face_nid_postfix(FreeType::GuestFaceRec* face); -int APS5_VABI FT_Reference_Face_nid_postfix(FreeType::GuestFaceRec* face); int APS5_VABI FT_Select_Size_nid_postfix(FreeType::GuestFaceRec* face, std::int32_t strikeIndex); int APS5_VABI FT_New_Size_nid_postfix(FreeType::GuestFaceRec* face, FreeType::GuestSizeRec** asize); int APS5_VABI FT_Activate_Size_nid_postfix(FreeType::GuestSizeRec* size); From 7d60d65e7a8cb5719172d39d5724d549fa2d3d85 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sun, 25 Oct 2026 22:49:22 +0300 Subject: [PATCH 425/428] test(sdk): reject third-party public header dependencies --- core/libs/tests/test_public_headers.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/core/libs/tests/test_public_headers.py b/core/libs/tests/test_public_headers.py index 8aa05f692b..8f76864b76 100644 --- a/core/libs/tests/test_public_headers.py +++ b/core/libs/tests/test_public_headers.py @@ -5,6 +5,7 @@ import json import re import subprocess import sys +import tempfile from functools import cache from pathlib import Path @@ -171,9 +172,19 @@ def compile_headers(compiler, compiler_args, groups, workers=4): def compile_one(group): includes = "".join(f'#include "{header.relative_to(LIBS).as_posix()}"\n' for header in group) - result = subprocess.run([compiler, *compiler_args, "-std=c++20", *warnings, "-fsyntax-only", "-x", "c++", f"-I{LIBS}", f"-I{ROOT / '3rdparty/freetype/include'}", "-"], input=includes * 2, text=True, capture_output=True) - if result.returncode: - raise RuntimeError(f"Header compilation failed: {group}\n{result.stdout}{result.stderr}") + with tempfile.TemporaryDirectory() as directory: + dependencies = Path(directory) / "headers.d" + result = subprocess.run([compiler, *compiler_args, "-std=c++20", *warnings, "-fsyntax-only", "-MD", "-MF", str(dependencies), "-MT", "public_headers", "-x", "c++", f"-I{LIBS}", f"-I{ROOT / '3rdparty/freetype/include'}", "-"], input=includes * 2, text=True, capture_output=True) + if result.returncode: + raise RuntimeError(f"Header compilation failed: {group}\n{result.stdout}{result.stderr}") + text = dependencies.read_text(encoding="utf-8").replace("\\\n", "") + if not text.startswith("public_headers:"): + raise RuntimeError(f"Invalid compiler dependency output: {dependencies}") + names = re.findall(r"(?:\\[ \t]|[^\s])+", text.removeprefix("public_headers:")) + paths = [Path(re.sub(r"\\([ \t#\\])", r"\1", name).replace("$$", "$")).resolve() for name in names] + forbidden = [path for path in paths if path.is_relative_to((ROOT / "3rdparty").resolve())] + if forbidden: + raise RuntimeError(f"Public headers depend on 3rdparty: {group}\n" + "\n".join(map(str, forbidden))) with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool: list(pool.map(compile_one, groups)) From b1b3069763837cf05143de62e416fbfd65595929 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sun, 25 Oct 2026 22:51:39 +0300 Subject: [PATCH 426/428] test(sdk): report all public header dependency failures --- core/libs/tests/test_public_headers.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/test_public_headers.py b/core/libs/tests/test_public_headers.py index 8f76864b76..2511659765 100644 --- a/core/libs/tests/test_public_headers.py +++ b/core/libs/tests/test_public_headers.py @@ -186,8 +186,16 @@ def compile_headers(compiler, compiler_args, groups, workers=4): if forbidden: raise RuntimeError(f"Public headers depend on 3rdparty: {group}\n" + "\n".join(map(str, forbidden))) + errors = [] with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool: - list(pool.map(compile_one, groups)) + futures = [(group, pool.submit(compile_one, group)) for group in groups] + for group, future in futures: + try: + future.result() + except Exception as error: + errors.append(f"{group}: {error}") + if errors: + raise RuntimeError(f"{len(errors)} header compilation units failed:\n\n" + "\n\n".join(errors)) def main(): From a0adf633ea40ad83803031fd4ecaed1f16469c8a Mon Sep 17 00:00:00 2001 From: boykopovar Date: Sun, 25 Oct 2026 22:56:38 +0300 Subject: [PATCH 427/428] refactor(freetype): decouple public list types from host headers --- .../prx/libSceFreeType/include/GuestList.hpp | 15 ++-- core/libs/prx/libSceFreeType/src/List.cpp | 90 +++++++++++-------- core/libs/tests/GuestFreeTypeList.cpp | 49 ++++++---- 3 files changed, 92 insertions(+), 62 deletions(-) diff --git a/core/libs/prx/libSceFreeType/include/GuestList.hpp b/core/libs/prx/libSceFreeType/include/GuestList.hpp index 67a8396b87..1e11a349c1 100644 --- a/core/libs/prx/libSceFreeType/include/GuestList.hpp +++ b/core/libs/prx/libSceFreeType/include/GuestList.hpp @@ -3,16 +3,21 @@ #include "prx/libc/include/general/VabiMacros.hpp" -#include -#include FT_LIST_H - #include #include namespace FreeType { -using GuestListRec = FT_ListRec; -using GuestListNodeRec = FT_ListNodeRec; +struct GuestListNodeRec { + GuestListNodeRec* prev; + GuestListNodeRec* next; + void* data; +}; + +struct GuestListRec { + GuestListNodeRec* head; + GuestListNodeRec* tail; +}; struct GuestMemoryRec { void* user; diff --git a/core/libs/prx/libSceFreeType/src/List.cpp b/core/libs/prx/libSceFreeType/src/List.cpp index 45a1872a31..198f3207b6 100644 --- a/core/libs/prx/libSceFreeType/src/List.cpp +++ b/core/libs/prx/libSceFreeType/src/List.cpp @@ -4,6 +4,8 @@ #include #include +#include +#include FT_LIST_H using namespace FreeType; @@ -26,77 +28,89 @@ private: std::array writes; }; -struct Iteration { - GuestListIterator iterator; - void* user; -}; - -int Iterate(FT_ListNode node, void* user) { - auto& call = *static_cast(user); - return call.iterator(node, call.user); -} - -struct Finalization { - GuestMemoryRec* memory; - GuestListDestructor destroy; - void* user; -}; - -void Destroy(FT_Memory memory, void* data, void*) { - auto& call = *static_cast(memory->user); - call.destroy(call.memory, data, call.user); -} - -void FreeNode(FT_Memory memory, void* block) { - auto& call = *static_cast(memory->user); - call.memory->free(call.memory, block); -} - } extern "C" { GuestListNodeRec* APS5_VABI FT_List_Find_nid_postfix(GuestListRec* list, void* data) { - return FT_List_Find(list, data); + if (!list) return nullptr; + for (auto* node = list->head; node; node = node->next) { + if (node->data == data) return node; + } + return nullptr; } void APS5_VABI FT_List_Add_nid_postfix(GuestListRec* list, GuestListNodeRec* node) { if (!list || !node) return; const ListWrite write(list, {node, list->tail}); - FT_List_Add(list, node); + auto* tail = list->tail; + node->next = nullptr; + node->prev = tail; + if (tail) tail->next = node; + else list->head = node; + list->tail = node; } void APS5_VABI FT_List_Insert_nid_postfix(GuestListRec* list, GuestListNodeRec* node) { if (!list || !node) return; const ListWrite write(list, {node, list->head}); - FT_List_Insert(list, node); + auto* head = list->head; + node->next = head; + node->prev = nullptr; + if (head) head->prev = node; + else list->tail = node; + list->head = node; } void APS5_VABI FT_List_Remove_nid_postfix(GuestListRec* list, GuestListNodeRec* node) { if (!list || !node) return; const ListWrite write(list, {node->prev, node->next}); - FT_List_Remove(list, node); + auto* previous = node->prev; + auto* next = node->next; + if (previous) previous->next = next; + else list->head = next; + if (next) next->prev = previous; + else list->tail = previous; } void APS5_VABI FT_List_Up_nid_postfix(GuestListRec* list, GuestListNodeRec* node) { if (!list || !node || !node->prev) return; const ListWrite write(list, {node, node->prev, node->next, list->head}); - FT_List_Up(list, node); + auto* previous = node->prev; + auto* next = node->next; + previous->next = next; + if (next) next->prev = previous; + else list->tail = previous; + node->prev = nullptr; + node->next = list->head; + list->head->prev = node; + list->head = node; } int APS5_VABI FT_List_Iterate_nid_postfix(GuestListRec* list, GuestListIterator iterator, void* user) { - Iteration call{iterator, user}; - return FT_List_Iterate(list, iterator ? Iterate : nullptr, &call); + if (!list || !iterator) return FT_Err_Invalid_Argument; + auto* node = list->head; + while (node) { + auto* next = node->next; + const int error = iterator(node, user); + if (error) return error; + node = next; + } + return 0; } void APS5_VABI FT_List_Finalize_nid_postfix(GuestListRec* list, GuestListDestructor destroy, GuestMemoryRec* memory, void* user) { if (!list || !memory) return; const ListWrite write(list); - Finalization call{memory, destroy, user}; - FT_MemoryRec_ native{}; - native.user = &call; - native.free = FreeNode; - FT_List_Finalize(list, destroy ? Destroy : nullptr, &native, user); + auto* node = list->head; + while (node) { + auto* next = node->next; + if (destroy) destroy(memory, node->data, user); + memory->free(memory, node); + node = next; + } + list->head = nullptr; + list->tail = nullptr; } } diff --git a/core/libs/tests/GuestFreeTypeList.cpp b/core/libs/tests/GuestFreeTypeList.cpp index 0e93a0d850..f853e00bbb 100644 --- a/core/libs/tests/GuestFreeTypeList.cpp +++ b/core/libs/tests/GuestFreeTypeList.cpp @@ -7,6 +7,9 @@ #include #include #include +#include +#include +#include FT_LIST_H using namespace FreeType; @@ -20,9 +23,11 @@ static void Check(bool value, int line) { #define Require(value) Check((value), __LINE__) constexpr std::size_t Count = 16; -using Nodes = std::array; +template +using Nodes = std::array; -static int Index(FT_ListNode node, const Nodes& nodes) { +template +static int Index(TNode* node, const Nodes& nodes) { if (!node) return -1; for (std::size_t i = 0; i < nodes.size(); ++i) if (node == &nodes[i]) return static_cast(i); Require(false); @@ -30,10 +35,10 @@ static int Index(FT_ListNode node, const Nodes& nodes) { } struct Lists { - FT_ListRec guest{}; + GuestListRec guest{}; FT_ListRec native{}; - Nodes actual{}; - Nodes expected{}; + Nodes actual{}; + Nodes expected{}; std::array values{11, 22, 33, 44}; Lists() { @@ -114,9 +119,10 @@ static void Mutations() { std::printf("list mutations: 2048 states compared with native FreeType\n"); } +template struct Walk { - FT_List list; - Nodes* nodes; + std::conditional_t, GuestListRec, FT_ListRec>* list; + Nodes* nodes; bool guest; bool remove; std::size_t stop; @@ -124,12 +130,13 @@ struct Walk { std::vector visits; }; -static int Visit(FT_ListNode node, void* user, bool guest) { - auto& state = *static_cast(user); +template +static int Visit(TNode* node, void* user, bool guest) { + auto& state = *static_cast*>(user); Require(state.guest == guest); state.visits.push_back(Index(node, *state.nodes)); if (state.remove) { - if (guest) FT_List_Remove_nid_postfix(state.list, node); + if constexpr (std::is_same_v) FT_List_Remove_nid_postfix(state.list, node); else FT_List_Remove(state.list, node); node->prev = nullptr; node->next = nullptr; @@ -172,8 +179,9 @@ struct Event { bool operator==(const Event&) const = default; }; +template struct Disposal { - FT_List list; + TList* list; FT_Memory native; GuestMemoryRec* guest; void* user; @@ -181,34 +189,36 @@ struct Disposal { std::vector events; }; -static void Release(Disposal& state, void* block) { +template +static void Release(Disposal& state, void* block) { const auto found = state.live.find(block); Require(found != state.live.end()); state.events.push_back({2, found->second, state.list->head == nullptr || state.list->tail == nullptr}); state.live.erase(found); - delete static_cast(block); + using Node = std::conditional_t, GuestListNodeRec, FT_ListNodeRec>; + delete static_cast(block); } static void NativeFree(FT_Memory memory, void* block) { - auto& state = *static_cast(memory->user); + auto& state = *static_cast*>(memory->user); Require(state.native == memory); Release(state, block); } static void APS5_VABI GuestFree(GuestMemoryRec* memory, void* block) { - auto& state = *static_cast(memory->user); + auto& state = *static_cast*>(memory->user); Require(state.guest == memory); Release(state, block); } static void NativeDestroy(FT_Memory memory, void* data, void* user) { - auto& state = *static_cast(memory->user); + auto& state = *static_cast*>(memory->user); Require(state.native == memory && state.user == user); state.events.push_back({1, data ? *static_cast(data) : -1, state.list->head == nullptr || state.list->tail == nullptr}); } static void APS5_VABI GuestDestroy(GuestMemoryRec* memory, void* data, void* user) { - auto& state = *static_cast(memory->user); + auto& state = *static_cast*>(memory->user); Require(state.guest == memory && state.user == user); state.events.push_back({1, data ? *static_cast(data) : -1, state.list->head == nullptr || state.list->tail == nullptr}); } @@ -216,7 +226,8 @@ static void APS5_VABI GuestDestroy(GuestMemoryRec* memory, void* data, void* use static void FinalizationCases() { for (const bool destroy : {false, true}) { for (const unsigned count : {0u, 1u, 8u}) { - FT_ListRec guestList{}, nativeList{}; + GuestListRec guestList{}; + FT_ListRec nativeList{}; std::array ids{0, 1, 2, 3, 4, 5, 6, 7}; int token = 9; Disposal guest{&guestList, nullptr, nullptr, &token, {}, {}}; @@ -226,7 +237,7 @@ static void FinalizationCases() { guest.guest = &guestMemory; native.native = &nativeMemory; for (unsigned i = 0; i < count; ++i) { - auto* a = new FT_ListNodeRec{}; + auto* a = new GuestListNodeRec{}; auto* b = new FT_ListNodeRec{}; a->data = b->data = i == 0 ? nullptr : &ids[i]; guest.live.emplace(a, i); From 725eb5fd30cf446164bf9d9061062091bd590f01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 25 Oct 2026 22:16:55 +0100 Subject: [PATCH 428/428] fix(main): compare only the x87 control and status words and MXCSR across thread switches Run 40226437381 failed guest_floating_point (feholdexcept workers preserve the main environment). The test compared the whole fnstenv image (tag word, FIP/FDP) of the main thread before and after other threads ran; a context switch re-initialises that state when the x87 unit is idle, so it flakes (3 of 28 main builds). Compare control word, status word and MXCSR. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestFloatingPoint.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestFloatingPoint.cpp b/core/libs/tests/GuestFloatingPoint.cpp index eb01ae2bc7..6d9afd5ae6 100644 --- a/core/libs/tests/GuestFloatingPoint.cpp +++ b/core/libs/tests/GuestFloatingPoint.cpp @@ -37,6 +37,10 @@ void WriteEnvironment(const Environment& state) { _mm_setcsr(state.sse); } +bool SameControlState(const Environment& left, const Environment& right) { + return left.x87[0] == right.x87[0] && left.x87[1] == right.x87[1] && left.sse == right.sse; +} + void Require(bool value, const char* message) { if (!value) { std::fprintf(stderr, "floating-point environment: %s\n", message); @@ -285,7 +289,7 @@ void CheckHoldExcept(HoldExcept hold) { try { hold(nullptr); } catch (const std::invalid_argument&) { threw = true; } Require(threw, "feholdexcept rejects null storage"); const auto afterNull = ReadEnvironment(); - Require(std::memcmp(&original, &afterNull, sizeof(original)) == 0, "null feholdexcept preserves the environment"); + Require(SameControlState(original, afterNull), "null feholdexcept preserves the environment"); #ifdef _WIN32 SYSTEM_INFO info{}; GetSystemInfo(&info); @@ -328,7 +332,7 @@ void CheckHoldExcept(HoldExcept hold) { first.join(); second.join(); const auto afterThreads = ReadEnvironment(); - Require(std::memcmp(&original, &afterThreads, sizeof(original)) == 0, "feholdexcept workers preserve the main environment"); + Require(SameControlState(original, afterThreads), "feholdexcept workers preserve the main environment"); } int main(int argc, char** argv) {