From e2f37f7289bc9bcced364be42eef23fdf91f9af5 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 00:45:29 +1300 Subject: [PATCH 01/18] feat(libc): implement the Dinkumware _Stod, _Stoll, _Sin and _Log helpers _Stod applies its power-of-ten scale to the decimal exponent before rounding, _Stoll is strtoll, and _Sin, _FSin, _LSin, _Log and _FLog pick the public function the firmware dispatches to. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libc/CMakeLists.txt | 10 +++ .../libs/prx/libc/src/DinkumwareInternals.cpp | 89 +++++++++++++++++++ core/libs/tests/GuestDinkumwareInternals.cpp | 84 +++++++++++++++++ 3 files changed, 183 insertions(+) create mode 100644 core/libs/prx/libc/src/DinkumwareInternals.cpp create mode 100644 core/libs/tests/GuestDinkumwareInternals.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index ca39154516..bead8a8d25 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -48,6 +48,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/HostEntropy.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Backtrace.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/DinkumwareAtomics.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/DinkumwareInternals.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Time.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AtomicBuiltins.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FileAndHeap.cpp @@ -447,6 +448,15 @@ if(BUILD_TESTING) set_tests_properties(guest_dinkumware_atomics PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_dinkumware_internals_tests ${LIBS_INCLUDE_DIR}/tests/GuestDinkumwareInternals.cpp) + target_include_directories(guest_dinkumware_internals_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_dinkumware_internals_tests PRIVATE libc) + configure_windows_unwind(guest_dinkumware_internals_tests) + add_test(NAME guest_dinkumware_internals COMMAND guest_dinkumware_internals_tests) + if(WIN32) + set_tests_properties(guest_dinkumware_internals PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_hostname_tests ${LIBS_INCLUDE_DIR}/tests/GuestHostname.cpp) target_include_directories(guest_hostname_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_hostname_tests PRIVATE libc) diff --git a/core/libs/prx/libc/src/DinkumwareInternals.cpp b/core/libs/prx/libc/src/DinkumwareInternals.cpp new file mode 100644 index 0000000000..82af11ba5e --- /dev/null +++ b/core/libs/prx/libc/src/DinkumwareInternals.cpp @@ -0,0 +1,89 @@ +#include +#include +#include +#include +#include +#include + +#include "prx/libc/include/General.hpp" + +extern "C" { +int* APS5_VABI __error_nid_postfix(); +double APS5_VABI strtod_nid_postfix(const char* str, char** endptr); +long long APS5_VABI strtoll_nid_postfix(const char* str, char** endptr, int base); +double APS5_VABI sin_nid_postfix(double x); +double APS5_VABI cos_nid_postfix(double x); +float APS5_VABI sinf_nid_postfix(float x); +float APS5_VABI cosf_nid_postfix(float x); +long double APS5_VABI sinl_nid_postfix(long double x); +long double APS5_VABI cosl_nid_postfix(long double x); +double APS5_VABI log_nid_postfix(double x); +double APS5_VABI log10_nid_postfix(double x); +double APS5_VABI log2_nid_postfix(double x); +float APS5_VABI logf_nid_postfix(float x); +float APS5_VABI log10f_nid_postfix(float x); +float APS5_VABI log2f_nid_postfix(float x); +} + +namespace { + +constexpr long long ExponentLimit = 1000000000; + +bool Hexadecimal(const char* text) { + while (std::isspace(static_cast(*text))) ++text; + if (*text == '+' || *text == '-') ++text; + return text[0] == '0' && (text[1] == 'x' || text[1] == 'X'); +} + +} + +extern "C" { + +double APS5_VABI _Stod_nid_postfix(const char* text, char** end, long pten) { + const int saved = *__error_nid_postfix(); + char* stop = nullptr; + const double value = strtod_nid_postfix(text, &stop); + if (end != nullptr) *end = stop; + if (pten == 0 || stop == text || !std::isfinite(value)) return value; + if (Hexadecimal(text)) NotImplemented_nid_no_patch("_Stod (hexadecimal text with a power-of-ten scale)"); + std::string number(text, static_cast(stop)); + long long exponent = 0; + const auto marker = number.find_first_of("eE"); + if (marker != std::string::npos) { + exponent = std::strtoll(number.c_str() + marker + 1, nullptr, 10); + number.resize(marker); + } + exponent = std::clamp(exponent, -ExponentLimit, ExponentLimit) + std::clamp(pten, -ExponentLimit, ExponentLimit); + number += 'e'; + number += std::to_string(exponent); + *__error_nid_postfix() = saved; + return strtod_nid_postfix(number.c_str(), nullptr); +} + +long long APS5_VABI _Stoll_nid_postfix(const char* text, char** end, int base) { + return strtoll_nid_postfix(text, end, base); +} + +double APS5_VABI _Sin_nid_postfix(double x, unsigned int quadrant) { + return quadrant != 0 ? cos_nid_postfix(x) : sin_nid_postfix(x); +} + +float APS5_VABI _FSin_nid_postfix(float x, unsigned int quadrant) { + return quadrant != 0 ? cosf_nid_postfix(x) : sinf_nid_postfix(x); +} + +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 _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); +} + +float APS5_VABI _FLog_nid_postfix(float x, int base) { + if (base > 0) return log10f_nid_postfix(x); + return base == 0 ? logf_nid_postfix(x) : log2f_nid_postfix(x); +} + +} diff --git a/core/libs/tests/GuestDinkumwareInternals.cpp b/core/libs/tests/GuestDinkumwareInternals.cpp new file mode 100644 index 0000000000..ccd3e04276 --- /dev/null +++ b/core/libs/tests/GuestDinkumwareInternals.cpp @@ -0,0 +1,84 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +int* APS5_VABI __error_nid_postfix(); +double APS5_VABI _Stod_nid_postfix(const char* text, char** end, long pten); +long long APS5_VABI _Stoll_nid_postfix(const char* text, char** end, int base); +double APS5_VABI _Sin_nid_postfix(double x, unsigned int quadrant); +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); +} + +namespace { + +constexpr int RangeError = 34; + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "Dinkumware internals: %s\n", message); + std::exit(1); + } +} + +void CheckStod(const char* text, long pten, double expected, long consumed, int error, const char* message) { + *__error_nid_postfix() = 0; + char* end = nullptr; + const double value = _Stod_nid_postfix(text, &end, pten); + const bool same = std::isnan(expected) ? std::isnan(value) : value == expected && std::signbit(value) == std::signbit(expected); + Require(same && end - text == consumed && *__error_nid_postfix() == error, message); +} + +void TestStod() { + CheckStod("1.5", 0, 1.5, 3, 0, "unscaled decimal"); + CheckStod("1.5", 1, 15.0, 3, 0, "decimal scaled up"); + CheckStod("12", -2, 0.12, 2, 0, "decimal scaled down"); + CheckStod(" -2.25e3xyz", 300, -0x1.a3f7f806a8135p+1007, 9, 0, "the scale is applied before rounding"); + CheckStod("1e-400", 300, 1e-100, 6, 0, "an underflowing text scaled back into range"); + CheckStod("1e-400", 0, 0.0, 6, RangeError, "an unscaled underflow"); + CheckStod("1e300", 100, HUGE_VAL, 5, RangeError, "a scale that overflows"); + CheckStod("1e300", 8, 1e308, 5, 0, "a scale up to the largest decades"); + CheckStod("5e-324", -1, 0.0, 6, RangeError, "a scale that underflows to zero"); + CheckStod("1", 2147483647, HUGE_VAL, 1, RangeError, "the largest scale"); + CheckStod("1e-2147483648", -1, 0.0, 13, RangeError, "the smallest exponent and scale"); + CheckStod("-0", 5, -0.0, 2, 0, "negative zero keeps its sign"); + CheckStod("infinity", 3, HUGE_VAL, 8, 0, "infinity is not scaled"); + CheckStod("nan", 3, NAN, 3, 0, "NaN is not scaled"); + CheckStod("1e", 2, 100.0, 1, 0, "an exponent marker without digits"); + CheckStod(".5", 2, 50.0, 2, 0, "a fraction without integer digits"); + CheckStod("0.3", -1, 0.03, 3, 0, "0.3 scaled down rounds as 0.03"); + CheckStod("123456789012345678901234567890", -20, 1234567890.1234567, 30, 0, "a long digit string"); + CheckStod("abc", 4, 0.0, 0, 0, "no number"); + CheckStod("0x1.8p0", 0, 1.5, 7, 0, "unscaled hexadecimal"); +} + +void TestStoll() { + char* end = nullptr; + *__error_nid_postfix() = 0; + Require(_Stoll_nid_postfix("-0x1F", &end, 0) == -31 && std::strcmp(end, "") == 0, "_Stoll base detection"); + Require(_Stoll_nid_postfix("+42abc", &end, 10) == 42 && std::strcmp(end, "abc") == 0, "_Stoll stops at the first non-digit"); + Require(_Stoll_nid_postfix(" z", &end, 36) == 35, "_Stoll base 36"); + Require(*__error_nid_postfix() == 0, "_Stoll set errno without an overflow"); + Require(_Stoll_nid_postfix("9223372036854775808", &end, 10) == 9223372036854775807ll && *__error_nid_postfix() == RangeError, "_Stoll saturates"); +} + +void TestDispatch() { + Require(_Sin_nid_postfix(0.5, 0) == std::sin(0.5) && _Sin_nid_postfix(0.5, 1) == std::cos(0.5) && _Sin_nid_postfix(0.5, 7) == std::cos(0.5), "_Sin"); + Require(_FSin_nid_postfix(0.5f, 0) == std::sin(0.5f) && _FSin_nid_postfix(0.5f, 2) == std::cos(0.5f), "_FSin"); + Require(_LSin_nid_postfix(0.5L, 0) == std::sin(0.5L) && _LSin_nid_postfix(0.5L, 3) == std::cos(0.5L), "_LSin"); + Require(_Log_nid_postfix(8.0, 0) == std::log(8.0) && _Log_nid_postfix(1000.0, 1) == 3.0 && _Log_nid_postfix(1000.0, 5) == 3.0 && _Log_nid_postfix(8.0, -1) == 3.0, "_Log"); + 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"); +} + +} + +int main() { + TestStod(); + TestStoll(); + TestDispatch(); +} From c5415e0845a8568cb4dccaf6c667275f2411752d Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 11:58:08 +1300 Subject: [PATCH 02/18] fix(libc): rescale _Stod text that overflows before scaling Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libc/src/DinkumwareInternals.cpp | 17 ++++++++++++----- core/libs/tests/GuestDinkumwareInternals.cpp | 8 ++++++-- docs/dev/debt/libc.md | 1 + 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libc/src/DinkumwareInternals.cpp b/core/libs/prx/libc/src/DinkumwareInternals.cpp index 82af11ba5e..163a7362fa 100644 --- a/core/libs/prx/libc/src/DinkumwareInternals.cpp +++ b/core/libs/prx/libc/src/DinkumwareInternals.cpp @@ -28,6 +28,7 @@ float APS5_VABI log2f_nid_postfix(float x); namespace { constexpr long long ExponentLimit = 1000000000; +constexpr int RangeError = 34; bool Hexadecimal(const char* text) { while (std::isspace(static_cast(*text))) ++text; @@ -39,12 +40,18 @@ bool Hexadecimal(const char* text) { extern "C" { -double APS5_VABI _Stod_nid_postfix(const char* text, char** end, long pten) { - const int saved = *__error_nid_postfix(); +double APS5_VABI _Stod_nid_postfix(const char* text, char** end, std::int64_t pten) { + int& guestErrno = *__error_nid_postfix(); + const int saved = guestErrno; + guestErrno = 0; char* stop = nullptr; const double value = strtod_nid_postfix(text, &stop); if (end != nullptr) *end = stop; - if (pten == 0 || stop == text || !std::isfinite(value)) return value; + const bool overflowed = std::isinf(value) && guestErrno == RangeError; + if (pten == 0 || stop == text || !(std::isfinite(value) || overflowed)) { + if (guestErrno == 0) guestErrno = saved; + return value; + } if (Hexadecimal(text)) NotImplemented_nid_no_patch("_Stod (hexadecimal text with a power-of-ten scale)"); std::string number(text, static_cast(stop)); long long exponent = 0; @@ -53,10 +60,10 @@ double APS5_VABI _Stod_nid_postfix(const char* text, char** end, long pten) { exponent = std::strtoll(number.c_str() + marker + 1, nullptr, 10); number.resize(marker); } - exponent = std::clamp(exponent, -ExponentLimit, ExponentLimit) + std::clamp(pten, -ExponentLimit, ExponentLimit); + exponent = std::clamp(exponent, -ExponentLimit, ExponentLimit) + std::clamp(pten, -ExponentLimit, ExponentLimit); number += 'e'; number += std::to_string(exponent); - *__error_nid_postfix() = saved; + guestErrno = saved; return strtod_nid_postfix(number.c_str(), nullptr); } diff --git a/core/libs/tests/GuestDinkumwareInternals.cpp b/core/libs/tests/GuestDinkumwareInternals.cpp index ccd3e04276..aa660dabdf 100644 --- a/core/libs/tests/GuestDinkumwareInternals.cpp +++ b/core/libs/tests/GuestDinkumwareInternals.cpp @@ -1,12 +1,13 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include extern "C" { int* APS5_VABI __error_nid_postfix(); -double APS5_VABI _Stod_nid_postfix(const char* text, char** end, long pten); +double APS5_VABI _Stod_nid_postfix(const char* text, char** end, std::int64_t pten); long long APS5_VABI _Stoll_nid_postfix(const char* text, char** end, int base); double APS5_VABI _Sin_nid_postfix(double x, unsigned int quadrant); float APS5_VABI _FSin_nid_postfix(float x, unsigned int quadrant); @@ -26,7 +27,7 @@ void Require(bool condition, const char* message) { } } -void CheckStod(const char* text, long pten, double expected, long consumed, int error, const char* message) { +void CheckStod(const char* text, std::int64_t pten, double expected, long consumed, int error, const char* message) { *__error_nid_postfix() = 0; char* end = nullptr; const double value = _Stod_nid_postfix(text, &end, pten); @@ -45,6 +46,9 @@ void TestStod() { CheckStod("1e300", 8, 1e308, 5, 0, "a scale up to the largest decades"); CheckStod("5e-324", -1, 0.0, 6, RangeError, "a scale that underflows to zero"); CheckStod("1", 2147483647, HUGE_VAL, 1, RangeError, "the largest scale"); + CheckStod("5", std::int64_t{1} << 40, HUGE_VAL, 1, RangeError, "a scale above 32 bits"); + CheckStod("1e400", -100, 1e300, 5, 0, "an overflowing text scaled back into range"); + CheckStod("0.01e41", -2, 1e37, 7, 0, "a negative scale from leading fraction zeros"); CheckStod("1e-2147483648", -1, 0.0, 13, RangeError, "the smallest exponent and scale"); CheckStod("-0", 5, -0.0, 2, 0, "negative zero keeps its sign"); CheckStod("infinity", 3, HUGE_VAL, 8, 0, "infinity is not scaled"); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 931d7d1746..dded6b4b39 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -69,6 +69,7 @@ - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [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 +- [_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 - [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) - [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 - [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 e321a04dca999ae22e37b735c47648ecda3f004b Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 12:06:22 +1300 Subject: [PATCH 03/18] docs(libc): move the _Stod debt entry clear of open edits Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- 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 1e7f5c75bb..18b7022d96 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -71,8 +71,8 @@ - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [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 -- [_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 - [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](../../../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 - [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. From c680e28f4f1feb02466acb85570c9859e069fb32 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 18:26:47 +1300 Subject: [PATCH 04/18] fix(libkernel): give sceCoredumpSetUserDataType its signature and result The call threw for lack of a signature. In the PS5 12.00 libkernel it takes one int, passes it to syscall 601 (command 29) and returns the raw kernel result. No dump is produced here, so it returns 0, as sceCoredumpConfigDumpMode does. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libkernel/System/src/Coredump.cpp | 4 ++-- core/libs/tests/GuestCoredumpUserData.cpp | 2 ++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Coredump.cpp b/core/libs/prx/libkernel/System/src/Coredump.cpp index 3bc229914c..c1a74bd344 100644 --- a/core/libs/prx/libkernel/System/src/Coredump.cpp +++ b/core/libs/prx/libkernel/System/src/Coredump.cpp @@ -69,8 +69,8 @@ int APS5_VABI sceCoredumpAttachMemoryRegionAsUserFile(uint32_t userValue, const return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } -int APS5_VABI sceCoredumpSetUserDataType(void) { - NotImplemented_nid_no_patch("Uxqkdta7wEg"); +int APS5_VABI sceCoredumpSetUserDataType(int type) { + (void)type; return 0; } diff --git a/core/libs/tests/GuestCoredumpUserData.cpp b/core/libs/tests/GuestCoredumpUserData.cpp index 30776ebbcc..bc23674815 100644 --- a/core/libs/tests/GuestCoredumpUserData.cpp +++ b/core/libs/tests/GuestCoredumpUserData.cpp @@ -14,6 +14,7 @@ int64_t APS5_VABI sceCoredumpWriteUserData(const void* data, size_t size); void APS5_VABI sceCoredumpDebugTextOut(const char* str, int len); int APS5_VABI sceCoredumpAttachUserFile(uint32_t userValue, const char* path); int APS5_VABI sceCoredumpAttachMemoryRegionAsUserFile(uint32_t userValue, const void* mem, size_t size, const char* name); +int APS5_VABI sceCoredumpSetUserDataType(int type); } namespace { @@ -61,6 +62,7 @@ int main() { const char text[] = "coredump text\n"; Require(NotInCoredumpHandler < 0); + for (const int type : {0, 1, 15, -1, 0x7fffffff}) Require(sceCoredumpSetUserDataType(type) == 0); RequireWritesRefused(region); RequireAttachesRefused(region); sceCoredumpDebugTextOut(text, static_cast(sizeof(text) - 1)); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 0aa068f431..27cd3a38fa 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -5,6 +5,7 @@ - [sceKernelGetSocPowerConsumption](../../../core/libs/prx/libkernel/System/src/ThermalSensors.cpp) (libkernel) - returns `EINVAL` and writes nothing, as `sceKernelGetSocSensorTemperature` does when the host has no sensor: the host's package power is not read (Linux RAPL `energy_uj` is readable by root only, and Windows has no general source). The prototype is not documented. PS5LM Plus (PPSA99581) passes a zeroed 128-byte buffer and, on success, reads its first 32-bit word as milliwatts, accepting 1000 to 350000; on failure it shows 0 W - [scePthreadMutexattrSetprotocol](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) - accepts 0 to 2 as a PS5 (firmware 13.60) does and returns `EINVAL` otherwise, but the protocol is not stored or applied: priority inheritance and protection are not modelled. On the console a null or uninitialized mutex attribute returns `EINVAL` from the SCE attribute calls, `scePthreadMutexInit` takes an uninitialized attribute as the default one, and a destroyed mutex returns `EINVAL` from `scePthreadMutexDestroy`; a null mutex pointer crashed the payload and still throws - [pthread_mutexattr_setprotocol, pthread_mutexattr_getprotocol, pthread_mutexattr_setprioceiling, pthread_mutexattr_getprioceiling](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) - the protocol and the priority ceiling are stored and reported as in [FreeBSD 11 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.0/lib/libthr/thread/thr_mutexattr.c#L192-L245) (the ceiling is set or read only under `PTHREAD_PRIO_PROTECT`, and setting a protocol resets it to the highest `SCHED_RR` priority). A null, uninitialized or destroyed attribute returns `EINVAL` before the other arguments are checked; this error behavior is unverified on the console. Mutexes do not apply the protocol or ceiling: a priority-inherit or priority-protect mutex locks like a normal one and does not change its owner's priority +- [sceCoredumpSetUserDataType](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `Uxqkdta7wEg`) - `(int type)` follows the PS5 12.00 `libkernel.sprx` at 0x138B0: the sign-extended `type` goes to syscall 601 (command 29, third argument 0), and the raw result comes back with no SCE code mapping, so a kernel error is -1 with `errno` set. Which types the kernel accepts is not measured. No dump is produced here, so every type returns 0. Palworld (PPSA23126, #2598) and Hello Neighbor (PPSA35154, discussion #6991) import it; #4144 left it throwing because no signature was known - [sceKernelGetMainSocId and sceKernelGetCpumode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) both return 0, and [sceKernelGetCpuFrequency](../../../core/libs/prx/libkernel/Time/Time.cpp) returns the host TSC frequency measured for `sceKernelGetTscFrequency`, not the console CPU clock. The PS5's SoC id and CPU mode are unknown; shadPS4's PS4 values are [0x710f10/0x740f30 and 0, 2 or 5 from the param.sfo CPU-mode flags](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1a/src/core/libraries/kernel/process.cpp). PPSA99203 only logs them (`[console] main SoC id: result=%#x out=%#x`, `CPU: %llu Hz, cpumode %d`), passing a pointer that `sceKernelGetMainSocId` leaves untouched - [sceKernelEnableDmemAliasing](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) returns 0 and changes nothing: direct memory can always be mapped more than once here, as `DoMapDirect` in [DirectMemory.cpp](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) only checks that the physical pages are allocated, so there is no aliasing to turn on. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L39-L43) (PS4) sets a flag there that lifts the `EBUSY` its [`sceKernelMapDirectMemory2`](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L299-L301), and [`sceKernelMapDirectMemory` for SDK 2.50 and later](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L237-L242), return for direct memory that is [already mapped](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/memory.cpp#L548-L553); that check is not applied here. The no-argument `int` signature follows shadPS4 and ProsperoEden (PPSA99008), which calls it before mapping the same direct memory again. Whether the PS5 refuses such a mapping without the call is unverified - [sceKernelInternalMemoryGetAvailableSize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) reports 0 bytes and returns 0: what the console's internal memory pool holds and how large it is are unknown. The `int (size_t*)` signature is from [xUtilty](https://github.com/Backporter/xUtilty/blob/328a856af1ef/source/SystemWrapper.cpp#L85) (PS4). PPSA99203 calls it once beside `sceKernelAvailableFlexibleMemorySize` while starting its GS renderer and never reads the result ([GS.cpp](https://github.com/Swordpdf/PS5SX2/blob/b4cf55e87c34/pcsx2/GS/GS.cpp#L233-L247)) From fde789cca887e37e059d98c4b880af1a8252d80c Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 19:52:47 +1300 Subject: [PATCH 05/18] test(libkernel): assert sceCoredumpSetUserDataType only for type 0 Which types the console kernel accepts is not measured, so the test no longer asserts success for -1 and INT_MAX. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/tests/GuestCoredumpUserData.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/tests/GuestCoredumpUserData.cpp b/core/libs/tests/GuestCoredumpUserData.cpp index bc23674815..2b363d6c07 100644 --- a/core/libs/tests/GuestCoredumpUserData.cpp +++ b/core/libs/tests/GuestCoredumpUserData.cpp @@ -62,7 +62,7 @@ int main() { const char text[] = "coredump text\n"; Require(NotInCoredumpHandler < 0); - for (const int type : {0, 1, 15, -1, 0x7fffffff}) Require(sceCoredumpSetUserDataType(type) == 0); + Require(sceCoredumpSetUserDataType(0) == 0); RequireWritesRefused(region); RequireAttachesRefused(region); sceCoredumpDebugTextOut(text, static_cast(sizeof(text) - 1)); From f347e96e38cc32284b8becc695424d2a7e7b6537 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 23:10:16 +1300 Subject: [PATCH 06/18] fix(libSceAudioOut): return 0 from sceAudioOut2LoTerminate and forward sceAudioOut2InitializeEx The PS5 module's sceAudioOut2LoTerminate stops the low-latency service and returns 0 whether or not it runs; sceAudioOut2LoInit starts no service here. sceAudioOut2InitializeEx stores its argument and runs sceAudioOut2Initialize. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp | 4 ++++ core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp | 5 +++++ core/libs/prx/libSceAudioOut/src/Unimplemented.cpp | 10 ---------- .../libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp | 5 ++++- docs/dev/debt/libSceAudioOut.md | 4 ++-- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp index eaba6e102d..eac5204b6d 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp @@ -22,6 +22,10 @@ int APS5_VABI sceAudioOut2LoInit(void) { return sceAudioOut2Initialize(); } +int APS5_VABI sceAudioOut2LoTerminate(void) { + return 0; +} + int APS5_VABI sceAudioOut2LoContextQueryMemory(const AudioOut2ContextParam* params, size_t* memory_size) { return sceAudioOut2ContextQueryMemory(params, memory_size); } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp index c647f607ea..fbe40625b2 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp @@ -15,6 +15,11 @@ int APS5_VABI sceAudioOut2Initialize(void) { return 0; } +int APS5_VABI sceAudioOut2InitializeEx(std::uint32_t param) { + (void)param; + return sceAudioOut2Initialize(); +} + int APS5_VABI sceAudioOut2Set3DLatency(int userId, std::uint32_t output) { (void)userId; if (output > MAX_SUPPORTED_3D_OUTPUT) return SCE_AUDIO_OUT2_ERROR_INVALID_PARAM; diff --git a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp index 02abcf4fd9..3457538766 100644 --- a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp +++ b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp @@ -380,11 +380,6 @@ int APS5_VABI sceAudioOut2DebugStateCtrl(void) { return 0; } -int APS5_VABI sceAudioOut2LoTerminate(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOut2ContextBedWrite(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -395,11 +390,6 @@ int APS5_VABI sceAudioOut2PortCreateEx(void) { return 0; } -int APS5_VABI sceAudioOut2InitializeEx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOut2Terminate(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp index 9a09ec4fba..cbca1cf604 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp @@ -9,6 +9,7 @@ extern "C" { int APS5_VABI sceAudioOut2Initialize(); +int APS5_VABI sceAudioOut2InitializeEx(std::uint32_t); int APS5_VABI sceAudioOut2LoInit(); int APS5_VABI sceAudioOut2LoTerminate(); int APS5_VABI sceAudioOut2Set3DLatency(int, std::uint32_t); @@ -122,7 +123,9 @@ void TestEnableChat() { void TestLowLatencyService() { Require(sceAudioOut2LoInit() == 0, "starting the low-latency service must be accepted"); Require(sceAudioOut2LoInit() == 0, "starting the low-latency service again must be accepted"); - Require(ThrowsRuntimeError([] { sceAudioOut2LoTerminate(); }), "stopping the low-latency service must throw"); + Require(sceAudioOut2LoTerminate() == 0, "stopping the low-latency service returns 0"); + Require(sceAudioOut2LoTerminate() == 0, "stopping a stopped low-latency service returns 0"); + Require(sceAudioOut2InitializeEx(0) == 0 && sceAudioOut2InitializeEx(1) == 0, "the extended initialization succeeds"); } } diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index f6e6901f1f..9d41d61a53 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -91,7 +91,7 @@ - [sceAudioOut2MbusSetSparkParam](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2AllAudioMainToHeadphoneForDiag](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2LoPortGetStateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2LoTerminate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) throws. It takes no arguments and returns an int, and stops the low-latency service ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L497-L498)), but what it does to open contexts and ports and what it returns when the service is not started are unknown, and its counterpart `sceAudioOut2Terminate` is unimplemented. +- [sceAudioOut2LoTerminate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp) (libSceAudioOut) - returns 0, as the PS5 12.00 module (0x248d0) does whether or not the low-latency service runs. The module disconnects from the audio server and clears the service tables; here `sceAudioOut2LoInit` starts no service and open contexts and ports are left as they are - [sceAudioOut2ReportRegisterHandler](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2ReportUnregisterHandler](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2GetTvCorrectionInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature @@ -102,7 +102,7 @@ - [sceAudioOut2DebugStateCtrl](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2ContextBedWrite](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2PortCreateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2InitializeEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2InitializeEx](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - the PS5 12.00 module (0x2a640) stores its 32-bit argument and runs `sceAudioOut2Initialize`; the argument's meaning is unknown and it is ignored - [sceAudioOut2Terminate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2PortRegister](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2PortUnregister](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature From e0f3e860a2f021103d782ce68fcd3430528e6d99 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 23:11:19 +1300 Subject: [PATCH 07/18] docs(libSceAudioOut): list sceAudioOut2LoTerminate under Silent stubs Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- docs/dev/debt/libSceAudioOut.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 9d41d61a53..b63e38cdf9 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -1,5 +1,9 @@ # libSceAudioOut technical debt +### Silent stubs + +- [sceAudioOut2LoTerminate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp) (libSceAudioOut) - returns 0, as the PS5 12.00 module (0x248d0) does whether or not the low-latency service runs. The module disconnects from the audio server and clears the service tables; here `sceAudioOut2LoInit` starts no service and open contexts and ports are left as they are + ### Unknown function info - [sceAudioOut2ContextAdvance and sceAudioOut2ContextPush](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) - with an SDL device, Advance snapshots each port's PCM and gain; Push consumes completed grains in order and a blocking Push waits for Advance when none is ready. Inferred from PPSA26358, which rewrites the same 512-frame, 12-channel float buffer on a producer thread and pushes on another thread. Console buffer ownership, snapshot timing and empty-queue errors are unverified. Completed grains remain queued until Push consumes them, so memory use grows with an unconsumed PCM backlog. The console backlog limit is unverified. A nonblocking Push without a completed grain throws; this is a runtime restriction, not a console measurement. Context destruction concurrent with active calls remains unsupported. Without an SDL device the existing wall-clock path remains. @@ -91,7 +95,6 @@ - [sceAudioOut2MbusSetSparkParam](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2AllAudioMainToHeadphoneForDiag](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2LoPortGetStateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature -- [sceAudioOut2LoTerminate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp) (libSceAudioOut) - returns 0, as the PS5 12.00 module (0x248d0) does whether or not the low-latency service runs. The module disconnects from the audio server and clears the service tables; here `sceAudioOut2LoInit` starts no service and open contexts and ports are left as they are - [sceAudioOut2ReportRegisterHandler](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2ReportUnregisterHandler](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature - [sceAudioOut2GetTvCorrectionInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature From c5df2f4ce045a07eba7cc29bec1e286b442a50c2 Mon Sep 17 00:00:00 2001 From: orezraey Date: Wed, 21 Oct 2026 08:43:54 -0300 Subject: [PATCH 08/18] fix(libSceNet): accept every sceNetMemoryAllocate flag as measured on a PS5 A PS5 (firmware 13.60) allocates with any flag value, zero-fills only for flag 2 and returns 64-byte aligned blocks. Record the measured behaviour of sceNetGetIfName, sceNetGetRandom, sceNetGetSystemTime, sceNetMemoryFree and sceNetUsleep, which already match the console, and keep only their open gaps. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNet/Export.cpp | 5 ++--- core/libs/tests/GuestSceNet.cpp | 32 +++++++++++++++++++++--------- docs/dev/debt/libSceNet.md | 9 +++------ 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index d6a9bd0b15..8723f16ddb 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -2250,12 +2250,11 @@ int APS5_VABI sceNetIoctl(void) { void* APS5_VABI sceNetMemoryAllocate(std::int64_t size, int flags) { constexpr int zeroFill = 2; - if ((flags & ~zeroFill) != 0) - throw std::runtime_error("sceNetMemoryAllocate: flags " + std::to_string(flags) + " are not supported"); + constexpr std::size_t blockAlignment = 64; if (size <= 0) return nullptr; void* memory = nullptr; try { - memory = GuestHeap::GuestHeapAllocate_nid_postfix(static_cast(size)); + memory = GuestHeap::GuestHeapAlign_nid_postfix(blockAlignment, static_cast(size)); } catch (const std::bad_alloc&) { return nullptr; } catch (const std::length_error&) { diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 3f00cfe6d4..4af7e936ae 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -297,12 +297,18 @@ static void CheckRandom() { Require(Failed(sceNetGetRandom(nullptr), 22)); std::array values{}; for (auto& value : values) Require(sceNetGetRandom(&value) == 0); + std::array guarded{0x5a5a5a5au, 0x5a5a5a5au}; + Require(sceNetGetRandom(guarded.data()) == 0 && guarded[1] == 0x5a5a5a5au && *sceNetErrnoLoc() == 22); Require(std::any_of(values.begin() + 1, values.end(), [&](std::uint32_t value) { return value != values[0]; })); } static void CheckNetMemory() { - Require(sceNetMemoryAllocate(0, 0) == nullptr); - Require(sceNetMemoryAllocate(-1, 2) == nullptr); + *sceNetErrnoLoc() = 33; + for (const std::int64_t size : {std::int64_t{0}, std::int64_t{-1}, INT64_MIN}) { + Require(sceNetMemoryAllocate(size, 0) == nullptr); + Require(sceNetMemoryAllocate(size, 2) == nullptr); + } + Require(*sceNetErrnoLoc() == 33); auto* zeroed = static_cast(sceNetMemoryAllocate(4096, 2)); Require(zeroed != nullptr); Require(std::all_of(zeroed, zeroed + 4096, [](std::uint8_t byte) { return byte == 0; })); @@ -313,14 +319,20 @@ static void CheckNetMemory() { Require(zeroed[4095] == 0xa5); sceNetMemoryFree(plain); sceNetMemoryFree(zeroed); - sceNetMemoryFree(nullptr); - bool threw = false; - try { - sceNetMemoryAllocate(16, 1); - } catch (const std::runtime_error&) { - threw = true; + for (const int flags : {1, 3, 4, 8, 0x10, 0x100, INT32_MIN}) { + auto* block = static_cast(sceNetMemoryAllocate(16, flags)); + Require(block != nullptr && reinterpret_cast(block) % 64 == 0); + if ((flags & 2) != 0) Require(std::all_of(block, block + 16, [](std::uint8_t byte) { return byte == 0; })); + std::memset(block, 0x5a, 16); + sceNetMemoryFree(block); } - Require(threw); + for (const std::int64_t size : {1, 7, 256, 65536}) { + void* block = sceNetMemoryAllocate(size, 0); + Require(block != nullptr && reinterpret_cast(block) % 64 == 0); + sceNetMemoryFree(block); + } + sceNetMemoryFree(nullptr); + Require(*sceNetErrnoLoc() == 33); } static void CheckUnboundSocketName() { @@ -896,6 +908,8 @@ static void CheckUsleep() { 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)); } static void CheckEpollIdent() { diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index fa35cc1780..76917cc204 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -38,19 +38,14 @@ - [sceNetGetIfList](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetIfListEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetIfListOnce](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetGetIfName](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(uint32_t index)` signature and the table of ten interface names (`lo0`, `eth0`, `eth1`, `dbg0`, `wlan0`, `wlan1`, `gbe0`, `bt0`, `phone0`, `pppoe0`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp); an index past the table sets `EINVAL` and returns an empty string. Whether the console has more names, or returns null, is unverified on hardware - [sceNetGetIfnameNumList](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetNameToIndex](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetGetRandom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(uint32_t* output)` signature and the `EINVAL` failure for a null output follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp); the value comes from the host's `std::random_device`, as `kern.rng_pseudo` in libkernel does. Unverified on hardware - [sceNetGetRouteInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetsockoptEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetGetSystemTime](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unverified on PS5 hardware: the `void(uint64_t*)` signature, process-relative microseconds and ignored null output follow [shadPS4 net.cpp](https://github.com/shadps4-emu/shadPS4/blob/d615b3887ba29025a59242e1a26f5b68bb176158/src/core/libraries/net/net.cpp#L1654-L1659). The clock is the same as `sceKernelGetProcessTime`, including `APS5_TIME_SCALE` - [sceNetGetSystemMemInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetInitParam](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetInetPtonEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - 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); IPv4 conversion and the ignored flags follow PS4 firmware 10.71, not yet confirmed on PS5. There 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 the first 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 0, each single bit up to 0x100, 0x10000, 0x80000000 and -1 gave the same results; other values were not measured. LF, FF and CR as terminators, and `NET_EINVAL` for a null `src` or `dst`, follow `sceNetInetPton` and were not measured for this function. IPv6 converts as `sceNetInetPton` does. - [sceNetIoctl](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetMemoryAllocate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int64_t size, int flags)` signature, flag 2 as zero fill and the null result for a size of 0 or less follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp). The block comes from the guest heap, and an allocation failure returns null without setting an errno. Any other flag bit throws, since its meaning is not known. Unverified on hardware -- [sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(void* memory)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/d5ba5fd06c49af29b8ec9200cad70bca676a9cd4/src/core/libraries/net/net.cpp); the block returns to the guest heap and a null pointer is ignored. Unverified on hardware - [sceNetSetDnsInfoToKernel](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetSetsockoptEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetSocketInternal](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature @@ -63,7 +58,6 @@ - [sceNetThreadCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetThreadExit](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetThreadJoin](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetUsleep](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int microseconds)` signature and the `EINVAL` failure for a negative count follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp); other counts sleep through libkernel's `sceKernelUsleep`, a cancellation point, and return 0. Whether the console takes the count as signed, and what it returns, is unverified on hardware - [sceNetShowIfconfigForBuffer](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetShowRouteForBuffer](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetShowNetstatForBuffer](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature @@ -178,6 +172,9 @@ ### Functional +- [sceNetGetRandom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) takes its value from the host's `std::random_device`, as `kern.rng_pseudo` in libkernel does; the console's generator is unknown. The `(uint32_t* output)` signature, the four-byte write and the `EINVAL` failure for a null output were measured on a PS5 (firmware 13.60) +- [sceNetMemoryAllocate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) takes its blocks from the guest heap; the console's pool size and what it returns once the pool is exhausted are unmeasured, and AnyPS5 returns null without setting an errno, as the console does for a size of 0 or less. A reused block allocated without flag 2 holds 0xdd bytes on the console and its previous contents in AnyPS5. Measured on a PS5 (firmware 13.60): a size of 0, -1 or `INT64_MIN` returns null and leaves `sceNetErrnoLoc` unchanged, flag 2 zero-fills, flags 1, 4, 8, 0x10, 0x100 and 0x80000000 have no visible effect, and every block of 1 to 65536 bytes was 64-byte aligned +- [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) - [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 3509bb528a093bd93e7882b8a5372aa8e86f09a7 Mon Sep 17 00:00:00 2001 From: Retr0 Date: Wed, 21 Oct 2026 15:26:46 +0300 Subject: [PATCH 09/18] fix(libSceNet): report EAGAIN when a stream receive times out on Windows FreeBSD 11.4's soreceive_generic waits in sbwait (sys/kern/uipc_socket.c, uipc_sockbuf.c), whose msleep_sbt on so_rcv.sb_timeo returns EWOULDBLOCK (35) for every socket type when SO_RCVTIMEO expires. On Windows the host socket is non-blocking and native_transfer enforces the timeout itself, but it reported WSAETIMEDOUT, which only datagram receives turned into EAGAIN; a stream receive failed with ETIMEDOUT (60). A receive that reaches the stored timeout now reports WSAEWOULDBLOCK, so every receive returns EAGAIN, and a host WSAETIMEDOUT still maps to ETIMEDOUT. --- core/libs/prx/libSceNet/Export.cpp | 17 ++++------------ core/libs/tests/GuestSceNet.cpp | 31 ++++++++++++++++++++++++++++++ docs/dev/debt/libSceNet.md | 2 +- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index d6a9bd0b15..403ad000f2 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -438,15 +438,6 @@ std::int64_t message_length(const NetMsghdr* message) { return static_cast(total); } -int receive_error(const Sock& socket) { -#ifdef _WIN32 - if (WSAGetLastError() == WSAETIMEDOUT && socket.type != NET_SOCK_STREAM && socket.native->receiveTimeout > 0) return NET_EAGAIN; -#else - (void)socket; -#endif - return native_error(); -} - bool message_flags_supported(int flags, int accepted) { return (flags & ~accepted) == 0; } @@ -474,7 +465,7 @@ auto native_transfer(const NativeSocketHandle& socket, int flags, bool send, TOp if (timeout > 0) { const auto remaining = deadline - std::chrono::steady_clock::now(); if (remaining <= std::chrono::steady_clock::duration::zero()) { - WSASetLastError(completeLength > 0 ? WSAEWOULDBLOCK : WSAETIMEDOUT); + WSASetLastError(send && completeLength == 0 ? WSAETIMEDOUT : WSAEWOULDBLOCK); return transferred > 0 ? static_cast(transferred) : static_cast(-1); } milliseconds = static_cast(std::min(std::chrono::ceil(remaining).count(), milliseconds)); @@ -1080,7 +1071,7 @@ int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { #ifdef _WIN32 if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) return static_cast(len); #endif - return result >= 0 ? result : fail(receive_error(socket)); + return result >= 0 ? result : fail(native_error()); } int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* from, uint32_t* fromlen) { @@ -1121,7 +1112,7 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* #ifdef _WIN32 if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) result = static_cast(len); #endif - if (result < 0) return fail(receive_error(socket)); + if (result < 0) return fail(native_error()); if (from && !native_to_guest_address(peer, from, fromlen)) return fail(NET_EAFNOSUPPORT); return result; } @@ -1239,7 +1230,7 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { }); if (received < 0) { #ifdef _WIN32 - if (!datagram || WSAGetLastError() != WSAEMSGSIZE) return fail(receive_error(socket)); + if (!datagram || WSAGetLastError() != WSAEMSGSIZE) return fail(native_error()); received = static_cast(buffer.size()); #else return fail(native_error()); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 3f00cfe6d4..098821d987 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -580,6 +580,36 @@ static void CheckReceiveTimeout(int receiver, int sender, const std::array= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + 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("stream-timeout-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 KernelTimeval timeout{0, 50000}; + Require(sceNetSetsockopt(client, 0xffff, 0x1006, &timeout, sizeof(timeout)) == 0); + char buffer[8]{}; + NetIovec in[] = {{buffer, sizeof(buffer)}}; + NetMsghdr receive{nullptr, 0, in, 1, nullptr, 0, 0}; + const auto start = std::chrono::steady_clock::now(); + Require(Failed(sceNetRecv(client, buffer, sizeof(buffer), 0), 35)); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(40)); + Require(Failed(sceNetRecvfrom(client, buffer, sizeof(buffer), 0, nullptr, nullptr), 35)); + Require(Failed(sceNetRecvmsg(client, &receive, 0), 35)); + const char payload[] = "late"; + Require(sceNetSend(accepted, payload, sizeof(payload), 0) == sizeof(payload)); + Require(sceNetRecv(client, buffer, sizeof(buffer), 0) == sizeof(payload) && std::strcmp(buffer, payload) == 0); + Require(sceNetSend(client, payload, sizeof(payload), 0) == sizeof(payload)); + std::memset(buffer, 0, sizeof(buffer)); + Require(sceNetRecv(accepted, buffer, sizeof(buffer), 0) == sizeof(payload) && std::strcmp(buffer, payload) == 0); + for (const int descriptor : {client, accepted, listener}) Require(sceNetSocketClose(descriptor) == 0); +} + static void CheckMicrosecondTimeouts(int receiver) { const int receiveTimeout = 50000; const int sendTimeout = 70000; @@ -1648,6 +1678,7 @@ int main() { stdio_readable[0] = 1; Require(select_nid_postfix(1, stdio_readable, nullptr, nullptr, poll_now) == 0 && stdio_readable[0] == 0); CheckReceiveTimeout(udp_receiver, udp_sender, address); + CheckStreamReceiveTimeout(); CheckMicrosecondTimeouts(udp_receiver); CheckMessages(udp_receiver, udp_sender, address); Require(sceNetSocketClose(udp_sender) == 0); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index fa35cc1780..a73d228e1d 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -182,7 +182,7 @@ - [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](../../../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 - [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. From b85e8f9e1576066e889611369981a0c6fc34e19f Mon Sep 17 00:00:00 2001 From: emirplus Date: Wed, 21 Oct 2026 19:48:31 +0300 Subject: [PATCH 10/18] fix(tools): read CMakeCache, graph and changed list with utf-8-sig in select_tests --- tools/select_tests.py | 17 +++++++++------- tools/tests/test_select_tests.py | 34 ++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/tools/select_tests.py b/tools/select_tests.py index efb972d0a1..a69c482e43 100644 --- a/tools/select_tests.py +++ b/tools/select_tests.py @@ -16,7 +16,7 @@ BATCH = 20000 class Build: def __init__(self, path): cache = {} - for line in (Path(path) / "CMakeCache.txt").read_text(encoding="utf-8").splitlines(): + for line in (Path(path) / "CMakeCache.txt").read_text(encoding="utf-8-sig").splitlines(): name, _, value = line.partition("=") cache[name.partition(":")[0]] = value self.path = os.path.normpath(cache["CMAKE_CACHEFILE_DIR"]) @@ -225,7 +225,10 @@ def escape(name): return name.replace("$", "$$").replace(":", "$:").replace(" ", "$ ") -def main(): +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser(description="Select the tests whose build inputs a change touches") sub = parser.add_subparsers(dest="command", required=True) d = sub.add_parser("dump", help="write the test graph of a built tree") @@ -241,15 +244,15 @@ def main(): c = sub.add_parser("ctest", help="run ctest on a tree, limited to the selected tests if select wrote any") c.add_argument("build") c.add_argument("arguments", nargs=argparse.REMAINDER, help="ctest arguments") - args = parser.parse_args() + args = parser.parse_args(argv) if args.command == "ctest": selected = Path(args.build, "select.tests") - extra = ["-I", selected.read_text(encoding="utf-8").strip()] if selected.exists() else [] + extra = ["-I", selected.read_text(encoding="utf-8-sig").strip()] if selected.exists() else [] return subprocess.run(["ctest", "--test-dir", args.build, *args.arguments, *extra]).returncode if args.command == "explain": try: - graph = json.loads(args.graph.read_text(encoding="utf-8")) - changed = [line.strip() for line in args.changed.read_text(encoding="utf-8").splitlines() if line.strip()] + graph = json.loads(args.graph.read_text(encoding="utf-8-sig")) + changed = [line.strip() for line in args.changed.read_text(encoding="utf-8-sig").splitlines() if line.strip()] reasons = {} picked, full = select(graph, changed, reasons) except (OSError, ValueError) as error: @@ -270,7 +273,7 @@ def main(): if not os.path.exists(args.graph): print("full run: no test graph") return 0 - graph = json.loads(Path(args.graph).read_text(encoding="utf-8")) + graph = json.loads(Path(args.graph).read_text(encoding="utf-8-sig")) changed = [line.strip() for line in Path(args.changed).read_text(encoding="utf-8-sig").splitlines() if line.strip()] picked, full = select(graph, changed) if picked is None: diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py index e86b16ccad..7dbeb88bee 100644 --- a/tools/tests/test_select_tests.py +++ b/tools/tests/test_select_tests.py @@ -230,6 +230,40 @@ class BuildErrorTests(unittest.TestCase): self.assertIn("CMAKE_CACHEFILE_DIR", result.stderr) self.assertNotIn("Traceback", result.stderr) + def test_cache_with_utf8_bom_is_read(self): + with tempfile.TemporaryDirectory() as directory: + cache = Path(directory) / "CMakeCache.txt" + cache.write_text("\ufeffCMAKE_CACHEFILE_DIR:INTERNAL=C:/work/build\nCMAKE_HOME_DIRECTORY:INTERNAL=C:/work/source\n", encoding="utf-8") + with mock.patch.object(select_tests.Build, "run", return_value="Build file encoding: UTF-8"): + build = select_tests.Build(directory) + self.assertEqual(build.path, os.path.normpath("C:/work/build")) + self.assertEqual(build.source, os.path.normpath("C:/work/source")) + + def test_explain_accepts_utf8_bom_in_graph_and_changed(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + graph_path = root / "graph.json" + graph = { + "tests": { + "unit": { + "target": "tests/unit", + "arguments": [], + "registered": [], + "sets": [], + "requires": [], + "depends": [], + } + }, + "closures": {"tests/unit": ["src/main.c"]}, + } + graph_path.write_text("\ufeff" + json.dumps(graph), encoding="utf-8") + changed_path = root / "changed.txt" + changed_path.write_text("\ufeffsrc/main.c\n", encoding="utf-8") + result = self.run_tool("explain", graph_path, changed_path) + self.assertEqual(result.returncode, 0, result.stderr) + data = json.loads(result.stdout) + self.assertEqual(data["tests"], ["unit"]) + class CaseInsensitivePathTests(unittest.TestCase): def build(self): From b7c5dd03fd0ce14f74aeb8c560fa9705c81e560f Mon Sep 17 00:00:00 2001 From: emirplus Date: Wed, 21 Oct 2026 19:53:43 +0300 Subject: [PATCH 11/18] fix(tools): handle unwritable seed output and reconfigure streams in configure_seed --- tools/configure_seed.py | 16 +++++++++++----- tools/tests/test_configure_seed.py | 8 ++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/tools/configure_seed.py b/tools/configure_seed.py index d1936ae21a..8e042f9722 100644 --- a/tools/configure_seed.py +++ b/tools/configure_seed.py @@ -25,7 +25,10 @@ def quote(value): return '"' + value.replace("\\", "\\\\").replace('"', '\\"').replace("$", "\\$") + '"' -def main(): +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser(description="Write the configure check results of a CMake cache as an initial cache file") parser.add_argument("cache", help="CMakeCache.txt of a configured tree") parser.add_argument("seed", help="file to pass to cmake -C") @@ -33,7 +36,7 @@ def main(): help="seed matching check names only; repeat to include several patterns") parser.add_argument("--exclude", action="append", default=[], metavar="GLOB", help="omit matching check names after inclusion; patterns are case-sensitive") - args = parser.parse_args() + args = parser.parse_args(argv) cache, seed = Path(args.cache), Path(args.seed) if cache.resolve() == seed.resolve() or (cache.exists() and seed.exists() and cache.samefile(seed)): parser.error("seed output would overwrite the input cache") @@ -44,9 +47,12 @@ def main(): found = {name: entry for name, entry in results.items() if (not args.include or any(fnmatch.fnmatchcase(name, pattern) for pattern in args.include)) and not any(fnmatch.fnmatchcase(name, pattern) for pattern in args.exclude)} - seed.parent.mkdir(parents=True, exist_ok=True) - seed.write_text("".join(f"set({quote(name)} {quote(value)} CACHE INTERNAL {quote(doc)})\n" - for name, (value, doc) in sorted(found.items())), encoding="utf-8") + try: + seed.parent.mkdir(parents=True, exist_ok=True) + seed.write_text("".join(f"set({quote(name)} {quote(value)} CACHE INTERNAL {quote(doc)})\n" + for name, (value, doc) in sorted(found.items())), encoding="utf-8") + except OSError as error: + parser.error(f"{seed}: {error}") print(f"{len(found)} check results") return 0 diff --git a/tools/tests/test_configure_seed.py b/tools/tests/test_configure_seed.py index 2cff72cd94..785b724136 100644 --- a/tools/tests/test_configure_seed.py +++ b/tools/tests/test_configure_seed.py @@ -62,6 +62,14 @@ class ConfigureSeedPathTests(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stderr) self.assertIn("HAVE_FEATURE", seed.read_text(encoding="utf-8")) + def test_unwritable_seed_is_reported_without_a_traceback(self): + seed_dir = self.root / "seed_dir" + seed_dir.mkdir() + result = self.run_seed(seed_dir) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertIn(str(seed_dir), result.stderr) + self.assertNotIn("Traceback", result.stderr) + def test_same_path_preserves_cache(self): self.assert_rejected(self.cache) From da1d9407ed495e8d6b29b913d030a831a374b604 Mon Sep 17 00:00:00 2001 From: Kovalev Vladislav Date: Wed, 21 Oct 2026 23:23:07 +0600 Subject: [PATCH 12/18] fix(tools): bound total SELF alignment padding --- docs/user/USAGE.md | 2 + tools/tests/test_unwrap_self.py | 96 ++++++++++++++++++++++++++++++--- tools/unwrap_self.py | 9 ++-- 3 files changed, 98 insertions(+), 9 deletions(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 672e35bbbf..c3bb6d2171 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -12,6 +12,8 @@ Select or drop multiple app ZIP archives to convert them into one ZIP bundle. Th ## Input and conversion +The SELF unwrapper refuses containers requiring more than 64 MiB of total alignment padding before allocating the output buffer. This limit counts padding across file-backed and empty segments, not segment data; a refusal preserves the input and any existing output file. + Use a clean ELF executable. Place its bundled ELF modules in `sce_module/`, `sce_modules/`, or `prx/` beside the input executable, or under the parent directory selected with `--sce-module-path `. The option accepts absolute paths and paths relative to the current working directory; do not include the `sce_module` directory itself. Only files directly inside the module directories are processed. SELF/fSELF containers are skipped with a warning; unwrap them before relinking if they need to be included. `tools/unwrap_self.py ` does that for a decrypted container: it places the segments at the offsets of the SELF segment records and restores `p_offset % p_align == p_vaddr % p_align`, and it refuses a container whose load segments all look encrypted. The command-line relinker skips an ELF module as a backup copy, with a message, when its file name extends the name of another ELF module in the same directory and both have the same SONAME, such as a dumper's `libX.prx.esbak` or `libX.prxbak` beside an ELF `libX.prx`; a single backup beside a SELF container is still relinked. `prx/` can coexist with either `sce_module/` or `sce_modules/`. Both `sce_module/` and `sce_modules/` present is an error. All three absent is an error unless an explicit `--module-dir` is supplied. ```text diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index 6ee6d2095e..7ba9a38d9d 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -473,7 +473,7 @@ class UnwrapTest(unittest.TestCase): def allocate(value): if isinstance(value, int): - self.assertLessEqual(value, len(container) + unwrap_self.MAX_EMPTY_LOAD_PADDING) + self.assertLessEqual(value, len(container) + unwrap_self.MAX_ALIGNMENT_PADDING) return bytearray(value) for previous in (None, b"previous output"): @@ -486,7 +486,7 @@ class UnwrapTest(unittest.TestCase): contextlib.redirect_stderr(io.StringIO()) as stderr: self.assertEqual(unwrap_self.main(), 2) self.assertEqual(stdout.getvalue(), "") - self.assertIn("empty load alignment requires more than", stderr.getvalue()) + self.assertIn("segment alignment requires more than", stderr.getvalue()) self.assertNotIn("Traceback", stderr.getvalue()) if previous is not None: self.assertEqual(output.read_bytes(), previous) @@ -494,20 +494,104 @@ class UnwrapTest(unittest.TestCase): self.assertFalse(output.exists()) self.assertEqual(source.read_bytes(), container) + def test_excessive_file_backed_padding_is_refused_before_allocation(self): + cases = [ + ([(PT_LOAD, 5, 0x1234, (1 << 40) - 1, 0x100, 1 << 40)], + [(0x2804, 0x800, 0x100)], {0x800: b"x" * 0x100}), + ([(PT_LOAD, 5, 0x1234, 0, 0x100, PAGE), + (SCE_COMMENT, 0, 0xB00, 0, 0x20, 1 << 40)], + [(0x2804, 0x800, 0x100)], {0x800: b"x" * 0x100, 0xB00: pattern(9, 0x20)}), + ] + source = Path(self.directory) / "input.self" + output = Path(self.directory) / "output.elf" + for segments, records, payloads in cases: + container = build_container(segments, records, payloads) + source.write_bytes(container) + + def allocate(value): + if isinstance(value, int): + self.assertLessEqual(value, len(container) + 64 * 1024 * 1024) + return bytearray(value) + + for previous in (None, b"previous output"): + with self.subTest(segments=segments, previous=previous): + output.unlink(missing_ok=True) + if previous is not None: + output.write_bytes(previous) + with mock.patch.object(unwrap_self, "bytearray", side_effect=allocate, create=True), \ + mock.patch.object(sys, "argv", ["unwrap_self.py", str(source), str(output)]), \ + contextlib.redirect_stdout(io.StringIO()) as stdout, \ + contextlib.redirect_stderr(io.StringIO()) as stderr: + self.assertEqual(unwrap_self.main(), 2) + self.assertEqual(stdout.getvalue(), "") + self.assertIn("alignment requires more than", stderr.getvalue()) + self.assertNotIn("Traceback", stderr.getvalue()) + self.assertEqual(source.read_bytes(), container) + if previous is None: + self.assertFalse(output.exists()) + else: + self.assertEqual(output.read_bytes(), previous) + + def test_alignment_padding_limit_is_cumulative(self): + for empty in (False, True): + with self.subTest(empty=empty): + segments = [(PT_LOAD, 5, 0x1234, 0x80, 0x20, 0x100), + (PT_LOAD, 6, 0x5678, 0x580, 0 if empty else 0x10, 0x1000)] + records = [(0x800, 0x800, 0x20)] + payloads = {0x800: pattern(1, 0x20)} + if not empty: + records.append((0x100800, 0xA00, 0x10)) + payloads[0xA00] = pattern(9, 0x10) + source = Path(self.directory) / "input.self" + output = Path(self.directory) / "output.elf" + container = build_container(segments, records, payloads) + source.write_bytes(container) + output.write_bytes(b"previous output") + with mock.patch.object(unwrap_self, "MAX_ALIGNMENT_PADDING", 0x15F): + with self.assertRaisesRegex(unwrap_self.Refused, "alignment requires more than"): + unwrap_self.unwrap(source, output) + self.assertEqual(output.read_bytes(), b"previous output") + self.assertEqual(source.read_bytes(), container) + with mock.patch.object(unwrap_self, "MAX_ALIGNMENT_PADDING", 0x160): + unwrap_self.unwrap(source, output) + data, headers = read_program_headers(output) + for header in headers: + self.assertEqual(header[2] % header[7], header[3] % header[7]) + self.assertEqual(data[headers[0][2]:headers[0][2] + 0x20], payloads[0x800]) + if not empty: + self.assertEqual(data[headers[1][2]:headers[1][2] + 0x10], payloads[0xA00]) + + def test_large_alignment_with_small_padding_remains_supported(self): + payload = b"x" * 0x100 + segments = [(PT_LOAD, 5, 0x1234, FIRST_REGION + 0x10, len(payload), 1 << 40)] + container = build_container(segments, [(0x2804, 0x800, len(payload))], {0x800: payload}) + source = Path(self.directory) / "input.self" + output = Path(self.directory) / "output.elf" + source.write_bytes(container) + with mock.patch.object(unwrap_self, "MAX_ALIGNMENT_PADDING", 0x10): + unwrap_self.unwrap(source, output) + data, headers = read_program_headers(output) + load = headers[0] + self.assertEqual(load[2] % load[7], load[3] % load[7]) + self.assertEqual(load[2], FIRST_REGION + 0x10) + self.assertEqual(data[load[2]:load[2] + load[5]], payload) + self.assertEqual(source.read_bytes(), container) + def test_empty_load_padding_limit_counts_only_added_bytes(self): - baseline, output = self.convert(build_container(default_segments(), default_records(), default_payloads())) + backed = [(*segment[:5], 1) for segment in default_segments()] + baseline, output = self.convert(build_container(backed, default_records(), default_payloads())) self.assertEqual(baseline.returncode, 0, baseline.stderr) baseline_size = output.stat().st_size limit = 0x100 for extra in (-1, limit, limit + 1): with self.subTest(extra=extra): address = 0x10000 + baseline_size + extra - segments = default_segments() + [(PT_LOAD, 6, 0, address, 0, 0x10000)] + segments = backed + [(PT_LOAD, 6, 0, address, 0, 0x10000)] container = build_container(segments, default_records(), default_payloads()) source = Path(self.directory) / "input.self" source.write_bytes(container) previous = output.read_bytes() - with mock.patch.object(unwrap_self, "MAX_EMPTY_LOAD_PADDING", limit): + with mock.patch.object(unwrap_self, "MAX_ALIGNMENT_PADDING", limit): if extra <= limit: unwrap_self.unwrap(source, output) data, headers = read_program_headers(output) @@ -515,7 +599,7 @@ class UnwrapTest(unittest.TestCase): self.assertEqual(headers[3][2], baseline_size + extra) self.assertEqual(headers[3][2] % headers[3][7], address % headers[3][7]) else: - with self.assertRaisesRegex(unwrap_self.Refused, "empty load alignment requires more than"): + with self.assertRaisesRegex(unwrap_self.Refused, "segment alignment requires more than"): unwrap_self.unwrap(source, output) self.assertEqual(output.read_bytes(), previous) diff --git a/tools/unwrap_self.py b/tools/unwrap_self.py index 1ce81f2e12..491c499633 100644 --- a/tools/unwrap_self.py +++ b/tools/unwrap_self.py @@ -43,7 +43,7 @@ RECORD_SIZE = 0x20 RECORD_HAS_SEGMENT = 0x800 RECORD_SEGMENT_SHIFT = 20 FIRST_REGION = 0x400 -MAX_EMPTY_LOAD_PADDING = 64 * 1024 * 1024 +MAX_ALIGNMENT_PADDING = 64 * 1024 * 1024 PT_LOAD = 1 PT_NOTE = 4 ENTROPY_LIMIT = 7.5 @@ -273,9 +273,11 @@ def unwrap(path, output, verbose=False): regions.sort(key=lambda region: region["old"]) program_offset = struct.unpack_from(" MAX_EMPTY_LOAD_PADDING: - raise Refused("empty load alignment requires more than %d bytes of output padding" % MAX_EMPTY_LOAD_PADDING) + padding += empty_end - cursor + if padding > MAX_ALIGNMENT_PADDING: + raise Refused("segment alignment requires more than %d bytes of output padding" % MAX_ALIGNMENT_PADDING) cursor = empty_end result = bytearray(cursor) From 048b67c1c48e0e72b95e070d1eb6484ca14c3156 Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Wed, 21 Oct 2026 19:43:24 +0200 Subject: [PATCH 13/18] fix(libSceNgs2.native): reset the system option to the module's defaults, 8 voice channels included --- .../libs/prx/libSceNgs2.native/src/System.cpp | 3 ++- .../tests/Ngs2HandleErrors.cpp | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNgs2.native/src/System.cpp b/core/libs/prx/libSceNgs2.native/src/System.cpp index 99eede7a7c..2fe15a7311 100644 --- a/core/libs/prx/libSceNgs2.native/src/System.cpp +++ b/core/libs/prx/libSceNgs2.native/src/System.cpp @@ -212,6 +212,7 @@ static Ngs2SystemOption DefaultSystemOption() { option.max_grain_samples = 512; option.num_grain_samples = 256; option.sample_rate = 48000; + option.max_voice_channels = 8; return option; } @@ -422,7 +423,7 @@ int APS5_VABI sceNgs2SystemRender(uintptr_t system_handle, const Ngs2RenderBuffe } int APS5_VABI sceNgs2SystemResetOption(Ngs2SystemOption* option) { - if (option == nullptr) APS5_INVALID_ARG_EX; + if (option == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; *option = DefaultSystemOption(); return SCE_NGS2_OK; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp index 56246ca178..c9a0094eda 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp @@ -1,5 +1,7 @@ #include "Ngs2Test.hpp" +#include +#include #include #include @@ -7,6 +9,28 @@ static void Status(int actual, int expected, const char* message) { if (actual != expected) throw std::runtime_error(message); } +static void CheckResetOption() { + Status(sceNgs2SystemResetOption(nullptr), static_cast(0x804A8010u), "null system option did not return the invalid out address error"); + constexpr std::uint64_t size = 0x90; + constexpr std::uint32_t maxGrainSamples = 512; + constexpr std::uint32_t numGrainSamples = 256; + constexpr std::uint32_t sampleRate = 48000; + constexpr std::uint32_t maxVoiceChannels = 8; + unsigned char expected[0x90] = {}; + std::memcpy(expected + 0x00, &size, sizeof(size)); + std::memcpy(expected + 0x6c, &maxGrainSamples, sizeof(maxGrainSamples)); + std::memcpy(expected + 0x70, &numGrainSamples, sizeof(numGrainSamples)); + std::memcpy(expected + 0x74, &sampleRate, sizeof(sampleRate)); + std::memcpy(expected + 0x78, &maxVoiceChannels, sizeof(maxVoiceChannels)); + static_assert(sizeof(Ngs2SystemOption) == sizeof(expected)); + Ngs2SystemOption option; + std::memset(&option, 0xAB, sizeof(option)); + Status(sceNgs2SystemResetOption(&option), SCE_NGS2_OK, "system option reset failed"); + Require(std::memcmp(&option, expected, sizeof(expected)) == 0); + Ngs2ContextBufferInfo query{}; + Status(sceNgs2SystemQueryBufferSize(&option, &query), SCE_NGS2_OK, "reset system option was rejected"); +} + int main() { try { constexpr int systemError = static_cast(0x804A8201u); @@ -39,6 +63,7 @@ int main() { Status(sceNgs2SystemDestroy(system, nullptr), SCE_NGS2_OK, "valid system destroy failed"); Status(sceNgs2SystemGetUserData(system, &value), systemError, "retired system returned the wrong error"); Require(value == sentinel); + CheckResetOption(); std::cout << "NGS2 PS5 handle errors and validation precedence checked\n"; return 0; } catch (const std::exception& error) { From a42182ef499531a3737307ff2b407a2c2eaadd7e Mon Sep 17 00:00:00 2001 From: thuanquang Date: Wed, 21 Oct 2026 19:08:36 +0700 Subject: [PATCH 14/18] fix(libSceNpTrophy2): return INVALID_TROPHY_ID for a trophy that is not in the title's data sceNpTrophy2GetTrophyInfo threw std::invalid_argument for a trophy id that is not in the title's trophyNN.ucp, so a title that asks for one stopped (PPSA14363 asks for trophy 19 of ids 0 to 18). KytyPS5 returns NP_TROPHY2_ERROR_INVALID_TROPHY_ID (0x8055390A) there without writing either output; return the same. A group that is not in the data still throws. Co-Authored-By: Claude --- .../include/NpTrophy2Constants.hpp | 1 + core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp | 4 +--- core/libs/tests/GuestNpTrophyData.cpp | 16 ++++++++++++++-- docs/dev/debt/libSceNpTrophy2.md | 2 +- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp b/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp index 6a853b4c28..58ec7f5e33 100644 --- a/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp +++ b/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp @@ -4,6 +4,7 @@ #include static constexpr int SCE_NP_TROPHY2_OK = 0; +static constexpr int SCE_NP_TROPHY2_ERROR_INVALID_TROPHY_ID = -2141898486; static constexpr int SCE_NP_TROPHY2_ERROR_ICON_FILE_NOT_FOUND = -2141898479; static constexpr int NP_TROPHY2_CONTEXT_DEFAULT = 1; diff --git a/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp b/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp index ce29da1513..cc32a75483 100644 --- a/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp @@ -82,9 +82,7 @@ int APS5_VABI sceNpTrophy2GetTrophyInfo(int context, int handle, int trophy_id, return SCE_NP_TROPHY2_OK; } const auto trophy = package->trophies.find(trophy_id); - if (trophy == package->trophies.end()) { - throw std::invalid_argument(std::string(__func__) + ": trophy " + std::to_string(trophy_id) + " is not in " + NpTrophy2::PackagePath(owner->serviceLabel)); - } + if (trophy == package->trophies.end()) return SCE_NP_TROPHY2_ERROR_INVALID_TROPHY_ID; if (details != nullptr) fillDetails(trophy->second, details); if (data != nullptr) fillData(*owner, trophy->second, data); return SCE_NP_TROPHY2_OK; diff --git a/core/libs/tests/GuestNpTrophyData.cpp b/core/libs/tests/GuestNpTrophyData.cpp index 5c4eaf025a..8bf4e7ad00 100644 --- a/core/libs/tests/GuestNpTrophyData.cpp +++ b/core/libs/tests/GuestNpTrophyData.cpp @@ -2,6 +2,7 @@ #include "prx/libSceNpTrophy2/include/NpTrophy2Types.hpp" #include "prx/libSceNpTrophy2/include/TrophyStore.hpp" +#include #include #include #include @@ -36,6 +37,7 @@ int APS5_VABI sceNpTrophy2GetTrophyInfoArray(int, int, uint32_t, uint32_t, NpTro } static constexpr int InitialUser = 0x10000000; +static constexpr int InvalidTrophyId = static_cast(0x8055390Au); static const char TropConf[] = R"({"defaultLanguage":"en-US","trophies":[ {"id":"0","grade":"P"}, @@ -168,8 +170,18 @@ static void ReportTrophyData(int context) { Require(details.trophy_grade == 1 && std::strcmp(details.name, "All of them") == 0, "the platinum is not reported"); const auto steps = Trophy(context, 5, &details); Require(details.target.type == 1 && details.target.value == 5 && !steps.unlocked && steps.progress.type == 1 && steps.progress.value == 0, "the progressive trophy does not report its target"); - NpTrophy2Data unknown{}; - Require(ThrowsInvalidArgument([context, &unknown] { sceNpTrophy2GetTrophyInfo(context, 1, 99, nullptr, &unknown); }), "a trophy that is not in the package was reported"); + const auto untouched = [](const void* bytes, std::size_t size) { + const auto* first = static_cast(bytes); + return std::all_of(first, first + size, [](unsigned char byte) { return byte == 0x5a; }); + }; + for (const int unknownId : {8, 99, -1}) { + NpTrophy2Details unknownDetails{}; + NpTrophy2Data unknown{}; + std::memset(&unknownDetails, 0x5a, sizeof(unknownDetails)); + std::memset(&unknown, 0x5a, sizeof(unknown)); + Require(sceNpTrophy2GetTrophyInfo(context, 1, unknownId, &unknownDetails, &unknown) == InvalidTrophyId, "a trophy that is not in the package did not return the invalid trophy id error"); + Require(untouched(&unknownDetails, sizeof(unknownDetails)) && untouched(&unknown, sizeof(unknown)), "a trophy that is not in the package wrote its details or data"); + } std::vector list(10); std::vector listed(10); diff --git a/docs/dev/debt/libSceNpTrophy2.md b/docs/dev/debt/libSceNpTrophy2.md index 4576f12de6..e6cb5cd197 100644 --- a/docs/dev/debt/libSceNpTrophy2.md +++ b/docs/dev/debt/libSceNpTrophy2.md @@ -14,7 +14,7 @@ - [libSceNpTrophy2 pending exports](../../../core/libs/prx/libSceNpTrophy2/Export.cpp) - 23 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/libSceNpTrophy2.c). ### Functional -- [Trophy queries](../../../core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp) report the title's [trophy data](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp) (`sce_sys/trophy2/trophyNN.ucp`) and the unlocks in the [trophy store](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp). Without the file, or when it is malformed, they report the old placeholders and `sceNpTrophy2GetTrophyInfoArray` lists only trophy 0. Icons are not read. Hidden trophies report their names and descriptions; whether the console blanks them before the unlock is not known. `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); the console's formula is not known. A trophy or group that is not in the file throws. A context that was not created or was destroyed reports placeholders and no unlocks and returns success; the console's errors for it are unknown. +- [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. - [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. From 6b80e0db0668665c42925ec24c403dcbca246169 Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Wed, 21 Oct 2026 07:38:48 -0500 Subject: [PATCH 15/18] test(libSceRtc): cover sceRtcInit and sceRtcEnd lifecycle --- core/libs/tests/GuestRtc.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index a5585f843d..f5ab7ac79b 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -67,6 +67,8 @@ int APS5_VABI sceRtcSetCurrentTick(const RtcTick*); int APS5_VABI sceRtcSetCurrentNetworkTick(const RtcTick*); int APS5_VABI sceRtcSetCurrentAdNetworkTick(const RtcTick*); int APS5_VABI sceRtcSetCurrentDebugNetworkTick(const RtcTick*); +int APS5_VABI sceRtcInit(void); +int APS5_VABI sceRtcEnd(void); } static void Require(bool value) { if (!value) std::abort(); } @@ -891,4 +893,6 @@ int main() { Require(sceRtcGetCurrentDebugNetworkTick(¤t) == 0 && current.tick >= debugClock.tick && current.tick < debugClock.tick + 1000000); Require(sceRtcGetCurrentRawNetworkTick(¤t) == 0 && current.tick > unixEpochTick); } + Require(sceRtcInit() == 0); + Require(sceRtcEnd() == 0); } From 9587c4467bdc6628567903a540d6a5e117880ad3 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 22 Oct 2026 07:54:41 +0000 Subject: [PATCH 16/18] test(agc): share the pipelined fill blocker with its lambda and end each case on an empty pipeline agc_driver_pipelined_fill failed under load with "a satisfied WAIT_REG_MEM skipped an overlapping queued write" (8 of 72 runs, 12 copies pinned to two cores, on main d6eafa54d0). When SatisfiedWaitKeepsUnrelatedQueuedWork returned, the draw pipeline still counted one item: the queue had gone idle before the committer popped its last item, which cleared within half a millisecond. SatisfiedWaitDrainsOverlappingQueuedWork then saw two outstanding items as soon as it queued its blocker, and its wait ended before the WAIT_REG_MEM asked for a drain. Finish() now waits until the draw pipeline is empty, so each case starts on an empty one. The blocker also takes #9647's shape: its state is shared with the queued lambda, and each case waits for the blocker to finish, so no lambda can read a blocker that went out of scope. That shape alone still failed 5 of 72 loaded runs; with the empty-pipeline wait, 144 loaded and 20 unloaded runs passed. The 500 ms sleep in SubmitBehind stays. It lets the queue worker pass the scratch fill's DISPATCH on an empty pipeline and block on the GPU lock the test holds. Without it the worker drains behind the unreleased blocker, and all 72 loaded runs failed with "the queue worker never reached the fill". No counter shows that state to wait on. --- .../tests/execution/PipelinedFill.cpp | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedFill.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedFill.cpp index 1c13bc169d..c501698a57 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedFill.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedFill.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -75,16 +76,29 @@ void AppendLabel(std::vector& words, std::uint32_t* target, std:: } struct Blocker { - std::atomic release{false}; + struct State { + std::atomic release{false}; + std::atomic done{false}; + }; + std::shared_ptr state = std::make_shared(); std::uint32_t* target = nullptr; - ~Blocker() { release = true; } + ~Blocker() { Release(); } + void Release() { state->release = true; } void Enqueue(std::vector writes = {}) { - DrawPipeline::Queue0().Enqueue([this] { - while (!release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + DrawPipeline::Queue0().Enqueue([state = state, target = target] { + while (!state->release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); if (target != nullptr) std::atomic_ref(*target).store(1); - std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex()); + { std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex()); } + state->done = true; }, std::move(writes)); } + void Join() const { + const auto start = std::chrono::steady_clock::now(); + while (!state->done.load()) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(20), "the blocker never ran"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } }; void SubmitBehind(Blocker& blocker, std::vector& words) { @@ -107,7 +121,7 @@ void ReleaseWhen(Blocker& blocker, std::size_t reached, std::size_t passed) { Require(now - start < std::chrono::seconds(20), "the queue worker never reached the fill"); std::this_thread::sleep_for(std::chrono::milliseconds(1)); } - blocker.release = true; + blocker.Release(); } void Finish() { @@ -118,6 +132,11 @@ void Finish() { std::_Exit(1); } idle.get(); + const auto start = std::chrono::steady_clock::now(); + while (DrawPipeline::Queue0().Outstanding() != 0) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(20), "the draw pipeline kept an item after the queue went idle"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output.data()), sizeof(Output)); AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Scratch.data()), sizeof(Scratch)); } @@ -135,6 +154,7 @@ void InputsAfterQueuedLabels() { SubmitBehind(blocker, words); ReleaseWhen(blocker, 4, 5); Finish(); + blocker.Join(); for (std::size_t i = 0; i < Output.size(); ++i) { const auto expected = i < 128 ? 0xababababu : 0xcafebabeu; Require(Output[i] == expected, "a fill read its control or pattern before the queued labels that write them, at " + std::to_string(i)); @@ -156,6 +176,7 @@ void FillIntoFullPipeline() { SubmitBehind(blocker, words); ReleaseWhen(blocker, depth, depth + 1); Finish(); + blocker.Join(); for (std::size_t i = 0; i + 2 < depth; ++i) Require(Labels[i] == i + 1, "a queued label was not stored"); for (std::size_t i = 0; i < Output.size(); ++i) Require(Output[i] == (i < 256 ? 0x5a5a5a5au : 0xcafebabeu), "a fill behind a full draw pipeline did not land at " + std::to_string(i)); } @@ -180,8 +201,9 @@ void SatisfiedWaitKeepsUnrelatedQueuedWork() { while (DrawPipeline::Queue0().Outstanding() < 2 && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); const bool keptQueued = DrawPipeline::Queue0().Outstanding() >= 2; - blocker.release = true; + blocker.Release(); Finish(); + blocker.Join(); Require(keptQueued, "a satisfied WAIT_REG_MEM drained unrelated queued work"); Require(marker == 1, "a label after a satisfied WAIT_REG_MEM did not execute"); } @@ -211,11 +233,12 @@ void SatisfiedWaitDrainsOverlappingQueuedWork() { std::this_thread::sleep_for(std::chrono::milliseconds(1)); const bool drained = DrawPipeline::DrainRequestsByOpcode(0x3c) > drainsBefore; const bool labelQueuedEarly = DrawPipeline::Queue0().Outstanding() >= 2; - blocker.release = true; + blocker.Release(); Require(submitted.wait_for(std::chrono::seconds(20)) == std::future_status::ready, "WAIT_REG_MEM submission did not finish after the overlapping write"); submitted.get(); Finish(); + blocker.Join(); Require(drained && !labelQueuedEarly, "a satisfied WAIT_REG_MEM skipped an overlapping queued write"); Require(marker == 1, "a label after an overlapping WAIT_REG_MEM did not execute"); } From cd0c8c1cf07d6e818673b6124c82c6c70616015d Mon Sep 17 00:00:00 2001 From: DangerChamp <77676230+DangerChamp@users.noreply.github.com> Date: Fri, 16 Oct 2026 02:07:17 +0100 Subject: [PATCH 17/18] fix(shader): reuse completed pixel lane proofs Memoize successful entry-lane proofs without retaining results that depend on active Phi cycles. Add regression coverage for saved EXEC-mask loops on narrower host subgroups. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Ondra97 --- .../tests/SavedExecMaskLoop.hpp | 19 +++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 13 ++ .../src/SpirvAlu/SpirvAluEmitterMath.cpp | 137 ++++++++++++------ 3 files changed, 127 insertions(+), 42 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/SavedExecMaskLoop.hpp diff --git a/core/libs/prx/libSceAgcDriver/tests/SavedExecMaskLoop.hpp b/core/libs/prx/libSceAgcDriver/tests/SavedExecMaskLoop.hpp new file mode 100644 index 0000000000..d6aa73bc18 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SavedExecMaskLoop.hpp @@ -0,0 +1,19 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_SAVEDEXECMASKLOOP_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_SAVEDEXECMASKLOOP_HPP + +#include +#include + +namespace AgcTest { + +inline std::vector SavedExecMaskLoop(std::uint32_t rounds) { + std::vector code{0xbea2047eu, 0xbea40380u}; + for (std::uint32_t step = 0; step < rounds; ++step) code.insert(code.end(), {0xbefe0422u, 0x7c080080u, 0x8aa26a7eu}); + const auto branch = 0xbf850000u | static_cast(2 - static_cast(code.size() + 3)); + code.insert(code.end(), {0x80248124u, 0xbf0a8424u, branch, 0xbefe0422u}); + return code; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 16e54ee281..5fe0acd0e1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -20,6 +20,7 @@ #include "IntermediateRepresentation/IrBuilder.hpp" #include "IntermediateRepresentation/IrProgram.hpp" #include "SpirvBackend/SpirvAnalysis.hpp" +#include "SavedExecMaskLoop.hpp" #if ANYPS5_ENABLE_SPIRV_TOOLS #include "SpirvBackend/SpirvOptimizer.hpp" #endif @@ -4088,6 +4089,18 @@ void verifyHalfWaveReduction() { {0u, 0x38000000u, 0x88000000u, spv::OpGroupNonUniformBitwiseOr, "half-wave reduction: the wave64 OR scan did not read lane 31 as a subgroup OR on 32 lanes"}, }}; for (const auto& reduction : scans) require(reductions(pixel(scan(reduction.identity, reduction.vector, reduction.scalar), 32u, true), reduction.reduce) == 1u, reduction.message); + auto loopScan = scan(0xffffffffu, 0x26000000u, 0x83800000u); + const auto savedMaskLoop = AgcTest::SavedExecMaskLoop(8u); + loopScan.insert(loopScan.begin(), savedMaskLoop.begin(), savedMaskLoop.end()); + auto helperLoop = loopScan; + helperLoop[0] = 0xbea204c1u; + expectFailure([&] { static_cast(pixel(helperLoop, 32u, true)); }, "v_readlane_b32 of lane 63 is outside the 32-lane host subgroup", "half-wave reduction: a loop initialized with helper lanes was proven to exclude them"); + auto upperHelperLoop = loopScan; + upperHelperLoop[0] = 0xbea2037eu; + upperHelperLoop.insert(upperHelperLoop.begin() + 1, 0xbea303c1u); + expectFailure([&] { static_cast(pixel(upperHelperLoop, 32u, true)); }, "v_readlane_b32 of lane 63 is outside the 32-lane host subgroup", "half-wave reduction: the low-word loop proof was reused for helper-inclusive upper lanes"); + require(reductions(pixel(loopScan, 32u, true), spv::OpGroupNonUniformUMin) == 1u, "half-wave reduction: a loop-carried saved EXEC mask exhausted the entry-lane proof on 32 lanes"); + require(halves(pixel(loopScan, 64u, true), spv::OpGroupNonUniformUMin, 0xffffffffu), "half-wave reduction: a loop-carried saved EXEC mask did not preserve the two half-wave UMin reductions"); auto comparedScan = scan(0u, 0x38000000u, 0x88000000u); *std::find(comparedScan.begin(), comparedScan.end(), 0xbe98047eu) = 0x8a986a7eu; comparedScan.insert(comparedScan.begin(), 0x7c080080u); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index d609c27286..0f4f8ac116 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -322,74 +323,126 @@ public: explicit EntryLaneWalk(IrShaderStage stage) : everyInvocationEnters(EveryInvocationEnters(stage)) {} bool ZeroBit(const IrValue* bit) { - bit = Resolved(bit); - if (bit == nullptr || !Spend()) return false; - if (bit->HasImmediate()) return bit->Type() == IrType::Bool && !bit->ImmediateBool(); - if (Is(bit, IrOpcode::LogicalAnd)) return HelperOrFalseConjunct(bit) || ZeroBit(bit->Argument(0)) || ZeroBit(bit->Argument(1)); - if (Is(bit, IrOpcode::SelectU1)) return ZeroBit(bit->Argument(1)) && ZeroBit(bit->Argument(2)); - if (Is(bit, IrOpcode::Phi)) return Incoming(bit, [this](const IrValue* incoming) { return ZeroBit(incoming); }); - if (Is(bit, IrOpcode::IEqual32)) return HelperTest(bit); - const auto words = LaneBitWords(bit); - return words && Word((*words)[0], 0u) && Word((*words)[1], 1u); + return Prove(bit, Proof::ZeroBit, [this](const IrValue* value) { + if (value->HasImmediate()) return value->Type() == IrType::Bool && !value->ImmediateBool(); + if (Is(value, IrOpcode::LogicalAnd)) return HelperOrFalseConjunct(value) || ZeroBit(value->Argument(0)) || ZeroBit(value->Argument(1)); + if (Is(value, IrOpcode::SelectU1)) return ZeroBit(value->Argument(1)) && ZeroBit(value->Argument(2)); + if (Is(value, IrOpcode::Phi)) return Incoming(value, [this](const IrValue* incoming) { return ZeroBit(incoming); }); + if (Is(value, IrOpcode::IEqual32)) return HelperTest(value); + const auto words = LaneBitWords(value); + return words && Word((*words)[0], 0u) && Word((*words)[1], 1u); + }); } private: + enum class Proof : std::uint8_t { ZeroBit, NotHelper, LowWord, HighWord, LowWord64, HighWord64 }; + using Key = std::pair; + struct Assumption { + std::size_t depth; + std::uint64_t order; + }; + static constexpr std::size_t Unbounded = static_cast(-1); + bool Spend() { if (budget == 0u) return false; --budget; return true; } - bool NotHelper(const IrValue* predicate) { - predicate = Resolved(predicate); - if (predicate == nullptr || !Spend()) return false; - if (predicate->HasImmediate()) return everyInvocationEnters && predicate->Type() == IrType::Bool && predicate->ImmediateBool(); - switch (predicate->Opcode()) { - case IrOpcode::LogicalAnd: return NotHelper(predicate->Argument(0)) || NotHelper(predicate->Argument(1)); - case IrOpcode::IEqual32: return HelperTest(predicate); - default: return ZeroBit(predicate); + template + bool Prove(const IrValue* value, Proof proof, TCheck&& check) { + value = Resolved(value); + if (value == nullptr) return false; + const Key key{value, proof}; + if (const auto found = settled.find(key); found != settled.end()) return found->second; + if (const auto found = active.find(key); found != active.end()) return Assume(found->second); + if (const auto found = assumed.find(key); found != assumed.end()) return Assume(found->second.depth); + if (!Spend()) return false; + const auto outer = dependency; + const auto depth = active.size(); + const auto mark = created; + dependency = Unbounded; + active.emplace(key, depth); + const bool result = check(value); + active.erase(key); + const auto inner = dependency; + const bool holds = result && inner >= depth; + for (auto entry = assumed.begin(); entry != assumed.end();) { + if (entry->second.order < mark) { + ++entry; + continue; + } + if (result && !holds) { + entry->second.depth = std::min(entry->second.depth, inner); + ++entry; + continue; + } + if (holds) settled.emplace(entry->first, true); + entry = assumed.erase(entry); } + if (result && !holds) assumed.emplace(key, Assumption{inner, created++}); + else settled.emplace(key, result); + dependency = result && !holds ? std::min(outer, inner) : outer; + return result; + } + + bool Assume(std::size_t depth) { + dependency = std::min(dependency, depth); + return true; + } + + bool NotHelper(const IrValue* predicate) { + return Prove(predicate, Proof::NotHelper, [this](const IrValue* value) { + if (value->HasImmediate()) return everyInvocationEnters && value->Type() == IrType::Bool && value->ImmediateBool(); + switch (value->Opcode()) { + case IrOpcode::LogicalAnd: return NotHelper(value->Argument(0)) || NotHelper(value->Argument(1)); + case IrOpcode::IEqual32: return HelperTest(value); + default: return ZeroBit(value); + } + }); } bool Word(const IrValue* word, std::uint32_t half) { - word = Resolved(word); - if (word == nullptr || !Spend()) return false; - if (word->HasImmediate()) return IsU32(word, 0u); - switch (word->Opcode()) { - case IrOpcode::CompositeExtractU32x4: { - const auto* ballot = Resolved(word->Argument(0)); - return IsU32(word->Argument(1), half) && Is(ballot, IrOpcode::Ballot) && NotHelper(ballot->Argument(0)); + return Prove(word, half == 0u ? Proof::LowWord : Proof::HighWord, [this, half](const IrValue* value) { + if (value->HasImmediate()) return IsU32(value, 0u); + switch (value->Opcode()) { + case IrOpcode::CompositeExtractU32x4: { + const auto* ballot = Resolved(value->Argument(0)); + return IsU32(value->Argument(1), half) && Is(ballot, IrOpcode::Ballot) && NotHelper(ballot->Argument(0)); + } + case IrOpcode::CompositeExtractU64: return IsU32(value->Argument(1), half) && Word64(value->Argument(0), half); + case IrOpcode::BitwiseAnd32: return Word(value->Argument(0), half) || Word(value->Argument(1), half); + case IrOpcode::BitwiseOr32: return Word(value->Argument(0), half) && Word(value->Argument(1), half); + case IrOpcode::Phi: return Incoming(value, [this, half](const IrValue* incoming) { return Word(incoming, half); }); + default: return false; } - case IrOpcode::CompositeExtractU64: return IsU32(word->Argument(1), half) && Word64(word->Argument(0), half); - case IrOpcode::BitwiseAnd32: return Word(word->Argument(0), half) || Word(word->Argument(1), half); - case IrOpcode::BitwiseOr32: return Word(word->Argument(0), half) && Word(word->Argument(1), half); - case IrOpcode::Phi: return Incoming(word, [this, half](const IrValue* incoming) { return Word(incoming, half); }); - default: return false; - } + }); } bool Word64(const IrValue* mask, std::uint32_t half) { - mask = Resolved(mask); - if (mask == nullptr || !Spend()) return false; - if (mask->HasImmediate()) return mask->Type() == IrType::U64 && static_cast(mask->ImmediateU64() >> (32u * half)) == 0u; - switch (mask->Opcode()) { - case IrOpcode::CompositeConstructU64: return Word(mask->Argument(half), half); - case IrOpcode::Phi: return Incoming(mask, [this, half](const IrValue* incoming) { return Word64(incoming, half); }); - default: return false; - } + return Prove(mask, half == 0u ? Proof::LowWord64 : Proof::HighWord64, [this, half](const IrValue* value) { + if (value->HasImmediate()) return value->Type() == IrType::U64 && static_cast(value->ImmediateU64() >> (32u * half)) == 0u; + switch (value->Opcode()) { + case IrOpcode::CompositeConstructU64: return Word(value->Argument(half), half); + case IrOpcode::Phi: return Incoming(value, [this, half](const IrValue* incoming) { return Word64(incoming, half); }); + default: return false; + } + }); } template - bool Incoming(const IrValue* phi, TCheck&& check) { - if (!phis.insert(phi).second) return true; + static bool Incoming(const IrValue* phi, TCheck&& check) { bool result = phi->ArgumentCount() != 0u; for (std::size_t index = 0; result && index < phi->ArgumentCount(); ++index) result = check(phi->Argument(index)); - phis.erase(phi); return result; } bool everyInvocationEnters; - std::unordered_set phis; + std::map active; + std::map assumed; + std::map settled; + std::size_t dependency = Unbounded; + std::uint64_t created = 0u; std::uint32_t budget = 2048u; }; From 1499ae961d82ea603149ff4816624c10002e9cfb Mon Sep 17 00:00:00 2001 From: Joey Fisher Date: Thu, 22 Oct 2026 09:56:06 -0600 Subject: [PATCH 18/18] test(agc): collect the idle publish test block before the label retile agc_idle_publish_wait fails on RADV (RX 570, Steam Deck, #9781) at the label flush check: the publish was not waited, it was dropped as stale. The idle flush's submission is the first to reference the test block's host import, and amdgpu validates a user pointer's pages with write faults at every submission, which clears the asynchronous userfaultfd write protection of the whole import. The test forces APS5_WRITE_WATCH_IMPORTS=watch (the probe in decideImportWatch would leave such imports compared), so the next collect reads every unit as stored by the CPU. The test retiled unit 5 without collecting first, so the label publish's own collect stamped the unit newer than the retile's generation and publishUnits dropped it; no copy was recorded and PendingWriteOverlaps was false. The test now collects before that retile, as it already did before the idle ones, and models the condition on every driver with an uncollected store older than the retile. The checks run in an order that names the dropped publish before the wait. --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 97f02e7f2f..8a0b4d0ddc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -4804,11 +4804,14 @@ void idlePublishWaitTests(const Device& device, Recorder& recorder) { recorder.Sync(); Require(words[2 * unit] == 0x44 && words[4 * unit - 1] == 0x45, "a CPU store made after the idle flush returned was overwritten by its publish"); Require(words[2 * unit + 1] == 0x22 && words[3 * unit - 1] == 0x22 && words[3 * unit] == 0x33 && words[1 * unit] == 0x11 && words[4 * unit] == 0x11, "the idle publish's bytes are wrong"); + words[5 * unit] = 0x11; + CollectWritesUncached(address, bytes); retile(address + 5 * unit, 0x55); StorageTexture::FlushAllPending("label"); - Require(recorder.PendingWriteOverlaps(address + 5 * unit, unit), "a publish of a flush other than the idle one was waited"); + const bool pending = recorder.PendingWriteOverlaps(address + 5 * unit, unit); recorder.Sync(); - Require(words[5 * unit] == 0x55 && words[6 * unit - 1] == 0x55, "the label flush's publish did not land"); + Require(words[5 * unit] == 0x55 && words[6 * unit - 1] == 0x55, "the label flush's publish did not land: the unit read as written after its retile, so the publish dropped it as stale"); + Require(pending, "a publish of a flush other than the idle one was waited"); std::cout << "idle publish wait verified\n"; }