From e887616e7429a3dca6cc731e9c6f317c9ff0169b Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:47:16 -0700 Subject: [PATCH 001/160] fix(libSceNet): read back SO_REUSEADDR and SO_BROADCAST in sceNetGetsockopt sceNetSetsockopt takes both, but sceNetGetsockopt failed them with EOPNOTSUPP. It now reads them from the host socket. FreeBSD's sogetopt returns so_options & option for the boolean options, so they read back as 0x4 and 0x20 when set. --- core/libs/prx/libSceNet/Export.cpp | 9 +++++++++ core/libs/tests/GuestSceNet.cpp | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 808cdc7364..6aae283d42 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -1563,6 +1563,15 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint case NET_SO_RCVTIMEO: value = socket.native->receiveTimeout; break; case NET_SO_SNDTIMEO: value = socket.native->sendTimeout; break; case NET_SO_ONESBCAST: value = socket.onesbcast; break; + case NET_SO_REUSEADDR: + case NET_SO_BROADCAST: { + int enabled = 0; + NativeLength size = sizeof(enabled); + if (::getsockopt(socket.native->value, SOL_SOCKET, optname == NET_SO_REUSEADDR ? SO_REUSEADDR : SO_BROADCAST, reinterpret_cast(&enabled), &size) != 0) + return fail(native_error()); + value = enabled != 0 ? optname : 0; + break; + } case NET_SO_ERROR: { int pending = 0; NativeLength size = sizeof(pending); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index fd59160c0c..e118665ba3 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -1511,6 +1511,10 @@ int main() { Require(setsockopt_nid_postfix(closed, 0xffff, 0x20, &enabled, sizeof(enabled)) == 0); Require(sceNetSetsockopt(closed, 0xffff, 0x20, &enabled, sizeof(enabled)) == 0); Require(sceNetSetsockopt(closed, 0xffff, 0x4, &enabled, sizeof(enabled)) == 0); + int option = 0; + std::uint32_t optionSize = sizeof(option); + Require(sceNetGetsockopt(closed, 0xffff, 0x20, &option, &optionSize) == 0 && option == 0x20 && optionSize == sizeof(option)); + Require(sceNetGetsockopt(closed, 0xffff, 0x4, &option, &optionSize) == 0 && option == 0x4); Require(close_nid_postfix(closed) == 0); Require(Failed(sceNetSocketClose(closed), 9)); CheckKernelTimeouts(); From aea804d9fda6287c7577d71f87984bd87ed5f433 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:47:19 -0700 Subject: [PATCH 002/160] fix(libkernel): getsockopt reads options of libSceNet sockets setsockopt reaches a socket created with sceNetSocket through the option setter libSceNet registers, but getsockopt looked the descriptor up only among libkernel's own sockets and failed, so a title could set an option on such a socket through the BSD call and not read it back. libSceNet now registers an option getter with libkernel next to its setter, which answers through sceNetGetsockopt, so SO_RCVTIMEO and SO_SNDTIMEO come back as a struct timeval. --- core/libs/prx/libSceNet/Export.cpp | 13 +++++++++++++ .../prx/libkernel/Socket/include/SocketPoll.hpp | 3 +++ core/libs/prx/libkernel/Socket/src/Socket.cpp | 10 ++++++++++ core/libs/prx/libkernel/Socket/src/Udp.cpp | 4 ++++ core/libs/tests/GuestSceNet.cpp | 10 ++++++++++ docs/dev/debt/libkernel.md | 2 +- 6 files changed, 41 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 6aae283d42..98e5af0cd0 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -892,6 +892,19 @@ bool SetSocketOptionForKernel(int descriptor, int level, int option, const void* const bool g_socketOptionSetterRegistered = (KernelSetSocketOptionSetter_nid_no_patch(&SetSocketOptionForKernel), true); +extern "C" int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint32_t* optlen); + +bool GetSocketOptionForKernel(int descriptor, int level, int option, void* value, std::uint32_t* length, int* error) { + { + std::lock_guard lk(g_mutex); + if (g_socks.count(descriptor) == 0) return false; + } + *error = sceNetGetsockopt(descriptor, level, option, value, length) == 0 ? 0 : *errno_slot(); + return true; +} + +const bool g_socketOptionGetterRegistered = (KernelSetSocketOptionGetter_nid_no_patch(&GetSocketOptionForKernel), true); + constexpr int GuestFcntlGetStatus = 3; constexpr int GuestFcntlSetStatus = 4; constexpr int GuestReadWrite = 2; diff --git a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp index 97e68db173..55956c9294 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp @@ -19,6 +19,7 @@ struct Entry { using Poller = int (*)(Entry* entries, int count, int timeoutMilliseconds); using Closer = bool (*)(int descriptor, int* error); using OptionSetter = bool (*)(int descriptor, int level, int option, const void* value, std::uint32_t length, int* error); +using OptionGetter = bool (*)(int descriptor, int level, int option, void* value, std::uint32_t* length, int* error); using StatusControl = bool (*)(int descriptor, int command, int flags, int* result, int* error); struct NativeOwner { int (*transfer)(NativeOwner* owner, bool send, void* buffer, int length) = nullptr; @@ -32,6 +33,8 @@ extern "C" void KernelSetSocketCloser_nid_no_patch(KernelSocketPoll::Closer clos extern "C" bool KernelCloseSocket_nid_no_patch(int descriptor, int* error); extern "C" void KernelSetSocketOptionSetter_nid_no_patch(KernelSocketPoll::OptionSetter setter); extern "C" bool KernelSetSocketOption_nid_no_patch(int descriptor, int level, int option, const void* value, std::uint32_t length, int* error); +extern "C" void KernelSetSocketOptionGetter_nid_no_patch(KernelSocketPoll::OptionGetter getter); +extern "C" bool KernelGetSocketOption_nid_no_patch(int descriptor, int level, int option, void* value, std::uint32_t* length, int* error); extern "C" void KernelSetSocketStatusControl_nid_no_patch(KernelSocketPoll::StatusControl control); extern "C" bool KernelControlSocketStatus_nid_no_patch(int descriptor, int command, int flags, int* result, int* error); extern "C" void KernelSetSocketNativeLookup_nid_no_patch(KernelSocketPoll::NativeLookup lookup); diff --git a/core/libs/prx/libkernel/Socket/src/Socket.cpp b/core/libs/prx/libkernel/Socket/src/Socket.cpp index 6cc428f617..5a6b521a8f 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -38,6 +38,7 @@ struct GuestTimeval { std::atomic g_socketPoller{nullptr}; std::atomic g_socketCloser{nullptr}; std::atomic g_socketOptionSetter{nullptr}; +std::atomic g_socketOptionGetter{nullptr}; std::atomic g_socketStatusControl{nullptr}; std::atomic g_socketNativeLookup{nullptr}; @@ -161,6 +162,15 @@ void KernelSetSocketOptionSetter_nid_no_patch(KernelSocketPoll::OptionSetter set g_socketOptionSetter.store(setter); } +void KernelSetSocketOptionGetter_nid_no_patch(KernelSocketPoll::OptionGetter getter) { + g_socketOptionGetter.store(getter); +} + +bool KernelGetSocketOption_nid_no_patch(int descriptor, int level, int option, void* value, std::uint32_t* length, int* error) { + const auto getter = g_socketOptionGetter.load(); + return getter != nullptr && getter(descriptor, level, option, value, length, error); +} + bool KernelSetSocketOption_nid_no_patch(int descriptor, int level, int option, const void* value, std::uint32_t length, int* error) { const auto setter = g_socketOptionSetter.load(); return setter != nullptr && setter(descriptor, level, option, value, length, error); diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 3b412c4a5a..f7a99d14d8 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -1102,6 +1102,10 @@ int APS5_VABI setsockopt_nid_postfix(int descriptor, int level, int option, } int APS5_VABI getsockopt_nid_postfix(int descriptor, int level, int option, void* value, std::uint32_t* length) { + int netError = 0; + if (!GuestSockets::IsOpen(descriptor) && KernelGetSocketOption_nid_no_patch(descriptor, level, option, value, length, &netError)) { + return netError == 0 ? 0 : Fail(netError); + } const auto socket = Lookup(descriptor); if (!socket) return -1; if (level != 0xffff && level != 6 && level != 41) return Fail(42); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index e118665ba3..919b8b6961 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -91,6 +91,7 @@ int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI close_nid_postfix(int); bool GuestSocketIsOpen_nid_no_patch(int); int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); +int APS5_VABI getsockopt_nid_postfix(int, int, int, void*, std::uint32_t*); int APS5_VABI fcntl_nid_postfix(int, int, ...); struct GuestPollEntry { int descriptor; @@ -1515,6 +1516,15 @@ int main() { std::uint32_t optionSize = sizeof(option); Require(sceNetGetsockopt(closed, 0xffff, 0x20, &option, &optionSize) == 0 && option == 0x20 && optionSize == sizeof(option)); Require(sceNetGetsockopt(closed, 0xffff, 0x4, &option, &optionSize) == 0 && option == 0x4); + option = 0; + optionSize = sizeof(option); + Require(getsockopt_nid_postfix(closed, 0xffff, 0x20, &option, &optionSize) == 0 && option == 0x20 && optionSize == sizeof(option)); + KernelTimeval receiveTimeout{0, 250000}; + Require(setsockopt_nid_postfix(closed, 0xffff, 0x1006, &receiveTimeout, sizeof(receiveTimeout)) == 0); + receiveTimeout = {}; + std::uint32_t timeoutSize = sizeof(receiveTimeout); + Require(getsockopt_nid_postfix(closed, 0xffff, 0x1006, &receiveTimeout, &timeoutSize) == 0 && timeoutSize == sizeof(receiveTimeout)); + Require(receiveTimeout.tv_sec == 0 && receiveTimeout.tv_usec == 250000); Require(close_nid_postfix(closed) == 0); Require(Failed(sceNetSocketClose(closed), 9)); CheckKernelTimeouts(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index d34f5f2425..876aed2f72 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -142,7 +142,7 @@ - [Shared memory](../../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. On Windows, anonymous objects are delete-on-close temporary files and `mmap` maps them as [regular files](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp); named objects remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a read event on a listening socket, here and in `kevent`, reports the number of completed connections waiting to be accepted in `data`, as FreeBSD 11.4's [`filt_solisten`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3384-L3392) reports `so_qlen`: on Linux a TCP listener's accept queue from `TCP_INFO` (`tcpi_unacked`, which Linux fills with the accept backlog for a listening socket), and elsewhere, or for a listener that is not TCP, 1 while a connection is waiting, as no host call reports that count. A write event reports the free space of the socket's send buffer in `data`, as FreeBSD 11.4's [`filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3358-L3382) reports `sbspace`: on Linux the host's `SO_SNDBUF` less the bytes `SIOCOUTQ` counts as queued, which include the host's per-packet overhead, so the space shrinks by somewhat more than the bytes sent. Windows has no query for that space and macOS is not wired up, so both report the whole `SO_SNDBUF`, which does not shrink as data is queued. Only guest sockets are supported; other descriptors and a low watermark above 1 throw - [sceKernelAddReadEvent on pipes](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) is the one exception to the sockets-only rule above: it also takes the read end of a pipe, as `kevent` does through FreeBSD's `filt_piperead`, and ignores the size argument there. Host streams, other descriptors and a write event on a pipe throw -- [close](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) and [setsockopt](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) reach a [libSceNet](../../../core/libs/prx/libSceNet/Export.cpp) socket through hooks libSceNet registers, as `select` polls them; the other libkernel socket calls do not. libSceNet numbers its sockets from 32 in its own table, so a CRT file descriptor and a socket can share a number; `close` then closes the socket. On the console both are one kernel descriptor space +- [close](../../../core/libs/prx/libkernel/File/src/Stdio.cpp), [setsockopt and getsockopt](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) reach a [libSceNet](../../../core/libs/prx/libSceNet/Export.cpp) socket through hooks libSceNet registers, as `select` polls them; the other libkernel socket calls do not. libSceNet numbers its sockets from 32 in its own table, so a CRT file descriptor and a socket can share a number; `close` then closes the socket. On the console both are one kernel descriptor space - [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards. Guest scalar transfers, positioned transfers, vectored transfers and seeks share a position lock across duplicated descriptors; a vector holds it for the whole operation. Independently opened descriptors and other processes do not share that lock, so vector transfers over their overlapping file ranges are not coordinated. Handles that are not disk files return `ESPIPE`. A pipe or console `readv` can return a short read when no more input is available; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done - [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards; positioned transfers, plain transfers and seeks of one descriptor are serialized per descriptor (shared across duplicated descriptors), so a concurrent transfer observes a stable file pointer - [sceKernelAioSetParam](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) writes the five values into the 20-byte scheduling struct the first argument points to, following the declarations in [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelAio.cs) and [ampr_emu](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/ps5/include/sce/_fs.h). Whether the console checks the values is unknown, so a null struct, a scheduling window or delayed count limit outside 0..128, a split flag other than 0 or 1 and a split size or chunk size above 0x1000000 (SharpProspero's maximums) throw. The scheduling settings are not used: `sceKernelAioInitializeImpl` ignores the struct and requests run synchronously on submit From 087fb758f5196d826a9edcbd92a195ee37b4247b Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:05:57 +0200 Subject: [PATCH 003/160] docs(debt): note how a title uses the value of sceSystemServiceParamGetInt Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceSystemService.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index c11d910836..95af66caed 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -18,7 +18,7 @@ - [sceSystemServiceLaunchWebBrowser](../../../core/libs/prx/libSceSystemService/Export.cpp): the native export takes one URI argument. Null input, the 6015-byte length limit and supported scheme checks are recovered. The host still returns kernel `EOPNOTSUPP` for valid input; a missing browser service's native error is unresolved. - [sceSystemServiceParamGetString](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name. As in a payload process on a PS5 (firmware 13.60), the name is written NUL-padded to the whole buffer, and a buffer under 65 bytes, a null buffer or any other parameter returns `SYSTEM_SERVICE_ERROR_PARAMETER` without writing. Parameters 0 to 40, 100 to 110, 1000 to 1010, -1 and `INT_MAX` were tried there; the others are assumed to fail the same way. PPSA26344 at `0xA87935` passes parameter 6 with a 65-byte buffer - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Parameter 400 returns the same number as parameter 1: PPSA01325 asks for 400 first, asks for 1 when that fails, and uses either answer as an index into the same 31 languages; in the PS5 10.01 and 12.60 `libSceSystemService` modules parameter 400 reads the same registry value as parameter 1, without parameter 1's mapping of newer language numbers by SDK version, and returns 0x80a10003 when an imported check that is not identified returns 1; the 4.03 module has no parameter 400. Neither the mapping nor that error is modelled, and the parameter's name is not known. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format and time format stay fixed -- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - parameters 1 to 5, 100 to 111, 200 to 213, 300 and 400 answer (see the integer system parameters entry); every other parameter, and a null value pointer, returns 0x80A10003 without writing the value. Inside a retail title on a PS5 (firmware 13.40, measured 2026-10-19 through ptrace remote calls in the game process) parameters 0, 6, 7 (game parental level), 8 to 20, 401, 999, 1000 (enter button assignment) and 1001 to 1008 returned 0x80A10003, as did a null value pointer; 1 to 5 and 400 answered, and 100 answered the value 0. A sweep of parameters 0 to 2048 in the same way on 2026-10-23 also found 101 to 111, 200 to 213 and 300 answering, each writing 4 bytes; every other parameter, and a null value pointer for 100 to 111, returned 0x80A10003 and left the value as it was +- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - parameters 1 to 5, 100 to 111, 200 to 213, 300 and 400 answer (see the integer system parameters entry); every other parameter, and a null value pointer, returns 0x80A10003 without writing the value. Inside a retail title on a PS5 (firmware 13.40, measured 2026-10-19 through ptrace remote calls in the game process) parameters 0, 6, 7 (game parental level), 8 to 20, 401, 999, 1000 (enter button assignment) and 1001 to 1008 returned 0x80A10003, as did a null value pointer; 1 to 5 and 400 answered, and 100 answered the value 0. A sweep of parameters 0 to 2048 in the same way on 2026-10-23 also found 101 to 111, 200 to 213 and 300 answering, each writing 4 bytes; every other parameter, and a null value pointer for 100 to 111, returned 0x80A10003 and left the value as it was. One title (PPSA13356, call at 0xAF350) asks for parameter 1 and does not read the result. - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the time zone (parameter 4) and summer time (parameter 5) follow the host's local time zone at the moment of the call. Titles compute the local time as UTC plus the time zone in minutes plus 60 minutes when summer time is 1 ([Apollo](https://github.com/bucanero/apollo-ps4/blob/252a9d5a187c463994263009851a179f4f910c49/source/exec_cmd.c#L70-L92), [GoldHEN's frame logger](https://github.com/GoldHEN/GoldHEN_Plugins_Repository/blob/6c3a06f53d9caebadfb0aeed9bb94119012010a7/plugin_src/frame_logger/source/main.c#L60-L82), and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/413d2e3518fb435f40971d238fe0ecae7ee551da/src/SharpProspero/Platform/SystemParameters.cs#L75-L79) calls them the offset from UTC in minutes and whether summer time is in effect), so the time zone is the host's current offset east of UTC less the summer time shift while summer time is in effect: 60 minutes on Linux, and on Windows the C runtime's own summer time bias (`_get_dstbias`), which is also what shifted its local time. For a host zone whose summer time is not one hour, Linux keeps the sum right and the standard offset wrong, and Windows the opposite; how the console reports such a zone is unknown. libkernel's `sceKernelConvertUtcToLocaltime`, `sceKernelConvertLocaltimeToUtc` and `sceKernelGettimezone` still use UTC - [sceSystemServiceInitializePlayerDialogParam](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - a null param throws: what the console does with it is not measured. The console run in [prosper](https://github.com/mattias800/prosper/blob/40d95db32efe1ad00106a5b9a84e018fffa9ecff/prosper/tests/data/console_oracle/nethttp.golden.tsv#L46) shows only the 0x40 bytes written (size 0x40, user ID -1 at 0xC, zeros), so the field names and the mode and target account fields follow [SharpProspero's struct](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L637-L655), and the `void` return [its declaration](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L419-L426); the console left 0x800693020 in `rax`, not an SCE error code. Closes when: the null case is measured on a PS5 From 302915bd42a1576bad1bd1bf533b4d5dba668b14 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:16:45 +0200 Subject: [PATCH 004/160] docs(debt): note how titles call sceContentExportFromData Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceContentExport.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/dev/debt/libSceContentExport.md b/docs/dev/debt/libSceContentExport.md index 01f0d64324..30da1a6375 100644 --- a/docs/dev/debt/libSceContentExport.md +++ b/docs/dev/debt/libSceContentExport.md @@ -4,6 +4,8 @@ - [Content export sessions](../../../core/libs/prx/libSceContentExport/Export.cpp): the host has no media library, so `sceContentExportFromDataWithThumbnail` throws for a valid session without invoking data callbacks. Session allocation does not reproduce guest allocator calls, worker creation or their failure paths. `sceContentExportGetProgress` throws because an idle session returns either zero or an invalid-state error according to the caller's SDK compatibility mode, which is not available here. Initialization and session lookup still throw for some invalid inputs instead of reproducing the console's error precedence and legacy SDK behavior. Session IDs now use the lowest free slot in 0–9; exhaustion, termination with live sessions and cancellation before an export return the confirmed errors. +- [sceContentExportFromData](../../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport) - throws as unimplemented. Two titles disassembled for this entry call it once each with 7 arguments: PPSA01746 (0xba0e14) between `sceContentExportStart` and `sceContentExportFinish`, treating any result other than 0 as failure, and the ContentManagement plugin of PPSA06465 (0x11f9), treating a negative result as failure and any other as success. What the arguments and the callback mean is not shown. + ### Unknown function info - [sceContentExportDataProvideFunction, sceContentExportFree, sceContentExportFrom and sceContentExportMalloc](../../../core/libs/prx/libSceContentExport/Export.cpp): their signatures remain unresolved; these names have no matching exports in the supplied module's C or assembly listing. From a5c5d9908a0c547fd553889dc4d840f0018d9e60 Mon Sep 17 00:00:00 2001 From: xTRACKGOD <135815363+xTRACKGOD@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:33:08 +0530 Subject: [PATCH 005/160] fix(tools): select tests for build artifact arguments --- tools/select_tests.py | 12 ++++++++---- tools/tests/test_select_tests.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/tools/select_tests.py b/tools/select_tests.py index 02b15cfbbe..3bcb0b703e 100644 --- a/tools/select_tests.py +++ b/tools/select_tests.py @@ -181,7 +181,10 @@ def dump(build, out): deps[current].add(path) listing, listed = build.tests() graph = listing["backtraceGraph"] - targets = {build.target(t) for t in listed} - {None} + arguments = {test["name"]: {build.output(path) for path in paths((test.get("command") or [])[1:])} - {None} + for test in listed} + outputs = set(build.outputs(sorted(set().union(*arguments.values())))) + targets = ({build.target(t) for t in listed} - {None}) | outputs nodes = build.walk(sorted(targets)) closures = {} for target in sorted(targets): @@ -204,13 +207,14 @@ def dump(build, out): registered.add(build.repo(graph["files"][graph["nodes"][node]["file"]])) node = graph["nodes"][node].get("parent") props = {p["name"]: p["value"] for p in test.get("properties", [])} - arguments = sorted({build.repo(a) for a in paths((test.get("command") or [])[1:])} - {None}) - tests[test["name"]] = {"registered": sorted(registered - {None}), "arguments": arguments, + inputs = {build.repo(a) for a in paths((test.get("command") or [])[1:])} - {None} + inputs.update(path for target in arguments[test["name"]] & outputs for path in closures[target]) + tests[test["name"]] = {"registered": sorted(registered - {None}), "arguments": sorted(inputs), "requires": props.get("FIXTURES_REQUIRED", []), "sets": props.get("FIXTURES_SETUP", []), "depends": props.get("DEPENDS", []), "target": build.target(test)} with open(out, "w", encoding="utf-8") as file: json.dump({"tests": tests, "closures": closures}, file) - print(f"{len(tests)} tests, {len(closures)} test executables, {len(deps)} objects with header deps") + print(f"{len(tests)} tests, {len(closures)} build targets, {len(deps)} objects with header deps") def select(graph, changed, reasons=None): diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py index a34bf96c11..e6493953f7 100644 --- a/tools/tests/test_select_tests.py +++ b/tools/tests/test_select_tests.py @@ -203,6 +203,35 @@ class SelectTests(unittest.TestCase): subprocess.run(["cmake", "-S", str(self.source), "-B", str(self.build)], check=True, capture_output=True) subprocess.run(["cmake", "--build", str(self.build)], check=True, capture_output=True) + def test_build_argument_inputs_select_their_consuming_tests(self): + cmake, source = self.source / "alpha/CMakeLists.txt", self.source / "alpha/tool.c" + header = self.source / "alpha/tool.h" + original = cmake.read_text(encoding="utf-8") + try: + source.write_text('#include "tool.h"\nint main(void) { return TOOL; }\n', encoding="utf-8") + header.write_text("#define TOOL 0\n", encoding="utf-8") + cmake.write_text(original + "add_executable(alpha_tool tool.c)\n" + "add_test(NAME alpha_tool_direct COMMAND alpha_tool)\n" + "add_test(NAME alpha_tool_argument COMMAND alpha_tests $)\n" + "add_test(NAME alpha_tool_definition COMMAND alpha_tests -DTOOL=$)\n" + "add_test(NAME alpha_not_built COMMAND alpha_tests ${CMAKE_BINARY_DIR}/missing.txt ${CMAKE_BINARY_DIR})\n", + encoding="utf-8") + subprocess.run(["cmake", "-S", str(self.source), "-B", str(self.build)], check=True, capture_output=True) + subprocess.run(["cmake", "--build", str(self.build)], check=True, capture_output=True) + graph = self.build / "argument-graph.json" + self.tool("dump", self.build, graph) + for changed in ("alpha/tool.c", "alpha/tool.h"): + with self.subTest(changed=changed): + self.assertEqual(self.select(changed, graph=graph)[1], + {"alpha_tool_direct", "alpha_tool_argument", "alpha_tool_definition", "script"}) + self.assertEqual(self.select("data/input.txt", graph=graph)[1], {"alpha_data", "script"}) + finally: + cmake.write_text(original, encoding="utf-8") + source.unlink(missing_ok=True) + header.unlink(missing_ok=True) + subprocess.run(["cmake", "-S", str(self.source), "-B", str(self.build)], check=True, capture_output=True) + subprocess.run(["cmake", "--build", str(self.build)], check=True, capture_output=True) + def test_retargeted_test_does_not_prune_its_previous_executable(self): cmake = self.source / "alpha/CMakeLists.txt" original = cmake.read_text(encoding="utf-8") From 6aa0ef1bb28ae254a0924c57df5b00e2bc72fdf3 Mon Sep 17 00:00:00 2001 From: Emanuele <118837721+imellag@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:07:05 +0200 Subject: [PATCH 006/160] build(libSceAudioIn): keep --exclude-libs off Apple's linker --- core/libs/prx/libSceAudioIn/CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAudioIn/CMakeLists.txt b/core/libs/prx/libSceAudioIn/CMakeLists.txt index f25187b9c8..61d4a1861e 100644 --- a/core/libs/prx/libSceAudioIn/CMakeLists.txt +++ b/core/libs/prx/libSceAudioIn/CMakeLists.txt @@ -1,7 +1,9 @@ add_library(libSceAudioIn SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceAudioIn PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) target_link_libraries(libSceAudioIn PRIVATE SDL2-static) -target_link_options(libSceAudioIn PRIVATE LINKER:--exclude-libs,ALL) +if(NOT APPLE) + target_link_options(libSceAudioIn PRIVATE LINKER:--exclude-libs,ALL) +endif() configure_windows_unwind(libSceAudioIn) if(BUILD_TESTING) From 0f998c15747ce4974c85476e7a2a0d6fb09f7e52 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Thu, 22 Oct 2026 06:23:50 +0200 Subject: [PATCH 007/160] fix(npcppwebapi): preserve aliased inputs during growth --- .../libs/prx/libSceNpCppWebApi/CMakeLists.txt | 10 ++- core/libs/prx/libSceNpCppWebApi/Export.cpp | 14 ++- .../tests/GuestNpCppWebApiIntrusivePtr.cpp | 40 ++++++++- core/libs/tests/GuestNpCppWebApiString.cpp | 90 ++++++++++++++++++- 4 files changed, 145 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceNpCppWebApi/CMakeLists.txt b/core/libs/prx/libSceNpCppWebApi/CMakeLists.txt index 2519e8cc81..d3ffa1ac9e 100644 --- a/core/libs/prx/libSceNpCppWebApi/CMakeLists.txt +++ b/core/libs/prx/libSceNpCppWebApi/CMakeLists.txt @@ -14,8 +14,14 @@ if(BUILD_TESTING) target_link_libraries(guest_np_cpp_web_api_string_tests PRIVATE libSceNpCppWebApi libc) configure_windows_unwind(guest_np_cpp_web_api_string_tests) add_test(NAME guest_np_cpp_web_api_string COMMAND guest_np_cpp_web_api_string_tests) + add_test(NAME guest_np_cpp_web_api_alias_append COMMAND guest_np_cpp_web_api_string_tests alias-append) + add_test(NAME guest_np_cpp_web_api_alias_string_vector COMMAND guest_np_cpp_web_api_string_tests alias-vector) + add_test(NAME guest_np_cpp_web_api_alias_intrusive_vector COMMAND guest_np_cpp_web_api_intrusive_ptr_tests alias-vector) + set_tests_properties(guest_np_cpp_web_api_alias_append guest_np_cpp_web_api_alias_string_vector + guest_np_cpp_web_api_alias_intrusive_vector PROPERTIES TIMEOUT 30) if(WIN32) - set_tests_properties(guest_np_cpp_web_api_string PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") + set_tests_properties(guest_np_cpp_web_api_string guest_np_cpp_web_api_alias_append + guest_np_cpp_web_api_alias_string_vector guest_np_cpp_web_api_alias_intrusive_vector + PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceNpCppWebApi/Export.cpp b/core/libs/prx/libSceNpCppWebApi/Export.cpp index 6ab09331ae..483b252812 100644 --- a/core/libs/prx/libSceNpCppWebApi/Export.cpp +++ b/core/libs/prx/libSceNpCppWebApi/Export.cpp @@ -854,10 +854,11 @@ static bool VectorGrow(VectorBody* body, const std::size_t elementSize) { static int VectorPushBackIntrusivePtr(IntrusivePtr* self, const IntrusivePtr* element) { VectorBody* body = VectorBodyFor(self, 1, __func__); if (body == nullptr) return -1; + const IntrusivePtr copy = *element; if (!VectorGrow(body, sizeof(IntrusivePtr))) return -1; auto* elements = static_cast(body->elements); - elements[body->size] = *element; - if (element->object != nullptr) ReferenceCount(element->object, ReferenceCountOffset).fetch_add(1); + elements[body->size] = copy; + if (copy.object != nullptr) ReferenceCount(copy.object, ReferenceCountOffset).fetch_add(1); body->size += 1; return 0; } @@ -865,7 +866,6 @@ static int VectorPushBackIntrusivePtr(IntrusivePtr* self, const IntrusivePtr* el static int VectorPushBackString(IntrusivePtr* self, const CommonString* element) { VectorBody* body = VectorBodyFor(self, 2, __func__); if (body == nullptr) return -1; - if (!VectorGrow(body, sizeof(CommonString))) return -1; CommonString copy = *element; copy.referenceCount = 0; copy.buffer = nullptr; @@ -877,6 +877,10 @@ static int VectorPushBackString(IntrusivePtr* self, const CommonString* element) std::memcpy(copy.buffer, element->buffer, length); copy.bufferSize = length; } + if (!VectorGrow(body, sizeof(CommonString))) { + std::free(copy.buffer); + return -1; + } static_cast(body->elements)[body->size] = copy; body->size += 1; return 0; @@ -1050,11 +1054,13 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common6String6appendEPKc(CommonString* self, char* buffer = static_cast(std::malloc(capacity)); if (buffer == nullptr) return -1; if (oldLength != 0) std::memcpy(buffer, self->buffer, oldLength); + std::memmove(buffer + oldLength, text, textLength + 1); std::free(self->buffer); self->buffer = buffer; self->bufferSize = capacity; + return 0; } - std::memcpy(self->buffer + oldLength, text, textLength + 1); + std::memmove(self->buffer + oldLength, text, textLength + 1); return 0; } diff --git a/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp b/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp index 13cc9a8491..ef203cc21b 100644 --- a/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp +++ b/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include using Deleter = void (APS5_VABI*)(void*); @@ -227,7 +229,43 @@ static void TestAdoptedVectorBodyDestroyedThroughDeleter() { Require(vector.object == nullptr); } -int main() { +static void TestVectorAliasedGrowth() { + unsigned cases = 0, moves = 0; + for (unsigned sourceIndex = 0; sourceIndex < 4; ++sourceIndex) { + int context = 0; + alignas(8) unsigned char object[ObjectSize]; + std::memset(object, 0x5a, sizeof(object)); + WriteCount(object, 0, 1); + deleterCalls = 0; + deletedObject = nullptr; + IntrusivePtr vector{}; + _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEEC1EPNS2_10LibContextE(&vector, &context); + IntrusivePtr owner{object, RecordDelete, &context}; + for (unsigned index = 0; index < 4; ++index) + Require(_ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEE8pushBackERKS8_(&vector, &owner) == 0); + for (unsigned size = 4; size < 129; ++size) { + IntrusivePtr* before = nullptr; + std::memcpy(&before, static_cast(vector.object) + 0x38, sizeof(before)); + const auto oldAddress = reinterpret_cast(before); + Require(_ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEE8pushBackERKS8_(&vector, &before[sourceIndex]) == 0); + IntrusivePtr* after = nullptr; + std::memcpy(&after, static_cast(vector.object) + 0x38, sizeof(after)); + moves += reinterpret_cast(after) != oldAddress; + Require(after[size].object == object && after[size].deleter == RecordDelete && after[size].libContext == &context); + Require(ReadCount(object, 0) == static_cast(size + 2)); + Require(OnlyCountChanged(object, 0)); + ++cases; + } + _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEED1Ev(&vector); + Require(ReadCount(object, 0) == 1 && deleterCalls == 0); + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEED1Ev(&owner); + Require(ReadCount(object, 0) == 0 && deleterCalls == 1 && deletedObject == object); + } + std::printf("aliased intrusive vector: %u insertions, %u moved growths\n", cases, moves); +} + +int main(int argc, char** argv) { + if (argc == 2 && std::strcmp(argv[1], "alias-vector") == 0) { TestVectorAliasedGrowth(); return 0; } Require(sizeof(IntrusivePtr) == 0x18); const Construct constructors[] = { diff --git a/core/libs/tests/GuestNpCppWebApiString.cpp b/core/libs/tests/GuestNpCppWebApiString.cpp index 6d50a21eb3..c08b3138dc 100644 --- a/core/libs/tests/GuestNpCppWebApiString.cpp +++ b/core/libs/tests/GuestNpCppWebApiString.cpp @@ -5,10 +5,18 @@ #include #include #include +#include +#include +#include namespace { -void Require(bool value) { if (!value) std::abort(); } +void Require(bool value, std::source_location at = std::source_location::current()) { + if (!value) { + std::fprintf(stderr, "requirement failed at %s:%u\n", at.file_name(), at.line()); + std::abort(); + } +} constexpr std::size_t kStringSize = 0x20; constexpr std::size_t kGuardSize = 0x10; @@ -233,9 +241,87 @@ void TestOfflineResponses() { Require(_ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi26setPlayerSessionPropertiesEiRKNS4_37ParameterToSetPlayerSessionPropertiesERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE() == static_cast(0x80552907)); } +unsigned TestAppendAliased() { + constexpr std::size_t lengths[] = {0, 1, 7, 8, 9, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128, 129}; + unsigned cases = 0, failures = 0, grown = 0; + int context = 0; + for (const auto length : lengths) { + std::string initial(length, 'a'); + for (std::size_t index = 0; index < length; ++index) initial[index] = static_cast('a' + index % 26); + for (std::size_t offset = 0; offset <= length; ++offset) { + Storage storage; + std::memset(storage.bytes, kFill, sizeof(storage.bytes)); + _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(reinterpret_cast(storage.bytes), &context); + Require(_ZN3sce2Np9CppWebApi6Common6String6appendEPKc(reinterpret_cast(storage.bytes), initial.c_str()) == 0); + const char* source = _ZNK3sce2Np9CppWebApi6Common6String5c_strEv(reinterpret_cast(storage.bytes)) + offset; + const std::string expected = initial + initial.substr(offset); + const auto oldCapacity = Load(storage, kBufferSizeOffset); + Require(_ZN3sce2Np9CppWebApi6Common6String6appendEPKc(reinterpret_cast(storage.bytes), source) == 0); + const auto capacity = Load(storage, kBufferSizeOffset); + grown += capacity != oldCapacity; + ++cases; + if (std::memcmp(_ZNK3sce2Np9CppWebApi6Common6String5c_strEv(reinterpret_cast(storage.bytes)), expected.c_str(), expected.size() + 1) != 0) { + if (failures == 0) std::fprintf(stderr, "aliased append corrupted length=%zu offset=%zu\n", length, offset); + ++failures; + } + Require(GuardIntact(storage)); + Require(Load(storage, kLibContextOffset) == &context); + Require(Load(storage, kReferenceCountOffset) == 0); + _ZN3sce2Np9CppWebApi6Common6StringD1Ev(reinterpret_cast(storage.bytes)); + Require(GuardIntact(storage)); + } + } + std::printf("aliased append: %u cases, %u growths, %u corrupt results\n", cases, grown, failures); + Require(grown != 0); + return failures; } -int main() { +unsigned TestVectorAliased() { + unsigned cases = 0, moves = 0; + for (unsigned sourceIndex = 0; sourceIndex < 4; ++sourceIndex) { + int context = 0; + VectorStorage vector; + Storage element; + std::memset(vector.bytes, kFill, sizeof(vector.bytes)); + std::memset(element.bytes, kFill, sizeof(element.bytes)); + _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEEC1EPNS2_10LibContextE(reinterpret_cast(vector.bytes), &context); + _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(reinterpret_cast(element.bytes), &context); + Require(_ZN3sce2Np9CppWebApi6Common6String6appendEPKc(reinterpret_cast(element.bytes), "independent string payload") == 0); + for (unsigned index = 0; index < 4; ++index) + Require(_ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE8pushBackERKS4_(reinterpret_cast(vector.bytes), element.bytes) == 0); + void* body = LoadBody(vector.bytes, kObjectOffset); + for (unsigned size = 4; size < 129; ++size) { + auto* before = LoadBody(body, kBodyElementsOffset); + const auto oldAddress = reinterpret_cast(before); + const void* source = before + sourceIndex * kStringSize; + Require(_ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE8pushBackERKS4_(reinterpret_cast(vector.bytes), source) == 0); + auto* after = LoadBody(body, kBodyElementsOffset); + moves += reinterpret_cast(after) != oldAddress; + Require(LoadBody(body, kBodySizeOffset) == size + 1); + const char* original = LoadBody(after + sourceIndex * kStringSize, kBufferOffset); + const char* copy = LoadBody(after + size * kStringSize, kBufferOffset); + Require(original != copy && std::strcmp(copy, "independent string payload") == 0); + Require(LoadBody(after + size * kStringSize, kLibContextOffset) == &context); + Require(LoadBody(after + size * kStringSize, kReferenceCountOffset) == 0); + ++cases; + } + _ZN3sce2Np9CppWebApi6Common6StringD1Ev(reinterpret_cast(element.bytes)); + const auto* elements = LoadBody(body, kBodyElementsOffset); + for (unsigned index = 0; index < 129; ++index) { + Require(std::strcmp(LoadBody(elements + index * kStringSize, kBufferOffset), "independent string payload") == 0); + } + _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEED1Ev(reinterpret_cast(vector.bytes)); + Require(GuardIntact(element) && VectorGuardIntact(vector)); + } + std::printf("aliased string vector: %u insertions, %u moved growths\n", cases, moves); + return 0; +} + +} + +int main(int argc, char** argv) { + if (argc == 2 && std::strcmp(argv[1], "alias-append") == 0) return TestAppendAliased() ? 1 : 0; + if (argc == 2 && std::strcmp(argv[1], "alias-vector") == 0) return TestVectorAliased(); TestConstructedStringIsEmpty(); TestNullLibContextIsStored(); TestAppendBuildsAndClearsBuffer(); From 29b6cc46afed2d6dadc6191986cb07ac6693eae1 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 21 Oct 2026 19:13:36 -0400 Subject: [PATCH 008/160] perf(agc): read APS5_NO_QUEUED_DRAW_READ_ORDER once in the held-read check DrawPipeline::HeldReads called std::getenv on every call. It runs for each StorageTexture::FlushPending, which InPlaceDrawInput calls while the draw committer records a draw. On Windows, UCRT's getenv takes the environment lock and scans the block on each call: on the title screen of Spirit of the Island (PPSA13934), 375 of the draw committer's 5124 steady-state samples (7.3 %) were in ucrtbase under HeldReads, against under 0.3 % for the range scan itself. The variable is now read once into a static, as WaitMemory.cpp already does for the same variable. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp index a1a5ebaab1..1c9839e0b1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp @@ -467,7 +467,8 @@ std::uint64_t DrawPipeline::DrainRequestsByOpcode(std::uint32_t opcode) { } bool DrawPipeline::HeldReads(std::uint64_t address, std::size_t bytes) { - if (bytes == 0 || std::getenv("APS5_NO_QUEUED_DRAW_READ_ORDER") != nullptr) return false; + static const bool unordered = std::getenv("APS5_NO_QUEUED_DRAW_READ_ORDER") != nullptr; + if (bytes == 0 || unordered) return false; const auto end = address + bytes; auto& pipeline = Queue0(); std::lock_guard lock(pipeline.mutex); From 005afcac1035f73e3b1c6231379d1dee31564204 Mon Sep 17 00:00:00 2001 From: JaceTheBlu Date: Sun, 25 Oct 2026 21:08:55 +0000 Subject: [PATCH 009/160] feat(libSceLibreSsl): implement X509_VERIFY_PARAM_set1_host The export threw and was declared without parameters. It follows int_x509_param_set_hosts of LibreSSL 2.7, the first release with this export: a name with an embedded NUL is refused, otherwise the host list is freed, and a non-empty name is copied with the guest strndup into a new one-name list. A null or empty name only clears the list. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceLibreSsl/Export.cpp | 5 -- core/libs/prx/libSceLibreSsl/include/Api.hpp | 2 +- .../libs/prx/libSceLibreSsl/src/X509Store.cpp | 44 ++++++++++++++ .../prx/libSceLibreSsl/tests/X509Store.cpp | 60 ++++++++++++++++++- docs/dev/debt/libSceLibreSsl.md | 4 +- 5 files changed, 107 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 6aec90b0ed..6899390d38 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -1603,11 +1603,6 @@ int APS5_VABI X509_STORE_add_lookup_nid_postfix(void) { return 0; } -int APS5_VABI X509_VERIFY_PARAM_set1_host_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI X509_VERIFY_PARAM_set1_ip_asc_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl/include/Api.hpp b/core/libs/prx/libSceLibreSsl/include/Api.hpp index a1ba0f9325..88145a2c3a 100644 --- a/core/libs/prx/libSceLibreSsl/include/Api.hpp +++ b/core/libs/prx/libSceLibreSsl/include/Api.hpp @@ -415,7 +415,7 @@ int APS5_VABI X509_STORE_add_lookup_nid_postfix(void); void APS5_VABI X509_STORE_free_nid_postfix(LibreSslX509Store* store); LibreSslX509Store* APS5_VABI X509_STORE_new_nid_postfix(void); int APS5_VABI X509_STORE_set_flags_nid_postfix(LibreSslX509Store* store, std::uint64_t flags); -int APS5_VABI X509_VERIFY_PARAM_set1_host_nid_postfix(void); +int APS5_VABI X509_VERIFY_PARAM_set1_host_nid_postfix(LibreSslX509VerifyParam* param, const char* name, std::size_t length); int APS5_VABI X509_VERIFY_PARAM_set1_ip_asc_nid_postfix(void); int APS5_VABI X509_VERIFY_PARAM_set_hostflags_nid_postfix(void); int APS5_VABI X509_check_issued_nid_postfix(void); diff --git a/core/libs/prx/libSceLibreSsl/src/X509Store.cpp b/core/libs/prx/libSceLibreSsl/src/X509Store.cpp index 13e5872e05..72d161c65e 100644 --- a/core/libs/prx/libSceLibreSsl/src/X509Store.cpp +++ b/core/libs/prx/libSceLibreSsl/src/X509Store.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include static_assert(sizeof(LibreSslX509VerifyParamId) == 56); @@ -78,6 +79,21 @@ char** Elements(const Stack* stack, const char* function) { return stack->data; } +bool Push(Stack* stack, void* element, const char* function) { + if (!stack) return false; + if (stack->numAlloc <= stack->num + 1) { + if (stack->numAlloc <= 0 || stack->num < 0) NotImplemented_nid_no_patch(function); + auto* grown = static_cast(realloc_nid_postfix(stack->data, static_cast(stack->numAlloc) * 2 * sizeof(char*))); + if (!grown) return false; + stack->data = grown; + stack->numAlloc *= 2; + } + stack->data[stack->num] = static_cast(element); + ++stack->num; + stack->sorted = 0; + return true; +} + bool HoldsElements(const Stack* stack, const char* function) { if (!stack || stack->num <= 0) return false; char** const elements = Elements(stack, function); @@ -167,4 +183,32 @@ int APS5_VABI X509_STORE_set_flags_nid_postfix(LibreSslX509Store* store, std::ui return 1; } +int APS5_VABI X509_VERIFY_PARAM_set1_host_nid_postfix(LibreSslX509VerifyParam* param, const char* name, std::size_t length) { + if (!param || !param->id) NotImplemented_nid_no_patch(__func__); + auto* id = param->id; + if (name && length == 0) length = std::strlen(name); + if (name && std::memchr(name, '\0', length)) return 0; + if (id->hosts) { + FreeStrings(static_cast(id->hosts), __func__); + id->hosts = nullptr; + } + if (!name || length == 0) return 1; + char* copy = strndup_nid_postfix(name, length); + if (!copy) return 0; + if (!id->hosts && !(id->hosts = NewStack(nullptr))) { + free_nid_postfix(copy); + return 0; + } + auto* hosts = static_cast(id->hosts); + if (!Push(hosts, copy, __func__)) { + free_nid_postfix(copy); + if (hosts->num == 0) { + FreeStack(hosts); + id->hosts = nullptr; + } + return 0; + } + return 1; +} + } diff --git a/core/libs/prx/libSceLibreSsl/tests/X509Store.cpp b/core/libs/prx/libSceLibreSsl/tests/X509Store.cpp index 28c4b10edb..6e0279e83e 100644 --- a/core/libs/prx/libSceLibreSsl/tests/X509Store.cpp +++ b/core/libs/prx/libSceLibreSsl/tests/X509Store.cpp @@ -147,13 +147,52 @@ void Unsupported() { RequireThrows([&] { X509_STORE_free_nid_postfix(store); }, "null element array"); } +const char* Host(const LibreSslX509VerifyParam* param, int index) { + const auto* hosts = static_cast(param->id->hosts); + return hosts && index < hosts->num ? hosts->data[index] : nullptr; +} + +int HostCount(const LibreSslX509VerifyParam* param) { + const auto* hosts = static_cast(param->id->hosts); + return hosts ? hosts->num : -1; +} + +void Hosts() { + auto* store = X509_STORE_new_nid_postfix(); + auto* param = store->param; + Require(X509_VERIFY_PARAM_set1_host_nid_postfix(param, "example.com", 0) == 1, "a terminated name is set"); + Require(HostCount(param) == 1 && std::strcmp(Host(param, 0), "example.com") == 0, "the name is copied into a one-name list"); + const auto* hosts = static_cast(param->id->hosts); + Require(hosts->comp == nullptr && hosts->numAlloc == 4 && hosts->sorted == 0, "the host list is a plain stack"); + Require(X509_VERIFY_PARAM_set1_host_nid_postfix(param, "www.example.org.extra", 15) == 1, "a counted name is set"); + Require(HostCount(param) == 1 && std::strcmp(Host(param, 0), "www.example.org") == 0, "set replaces the list and copies the counted bytes"); + char unterminated[] = {'a', '.', 'b'}; + Require(X509_VERIFY_PARAM_set1_host_nid_postfix(param, unterminated, sizeof(unterminated)) == 1 && std::strcmp(Host(param, 0), "a.b") == 0, + "a counted name needs no terminator"); + const char embedded[] = {'a', '\0', 'b'}; + Require(X509_VERIFY_PARAM_set1_host_nid_postfix(param, embedded, sizeof(embedded)) == 0, "an embedded NUL is refused"); + Require(HostCount(param) == 1 && std::strcmp(Host(param, 0), "a.b") == 0, "a refused name keeps the list"); + Require(X509_VERIFY_PARAM_set1_host_nid_postfix(param, "", 0) == 1 && param->id->hosts == nullptr, "an empty name clears the list"); + Require(X509_VERIFY_PARAM_set1_host_nid_postfix(param, "c.d", 0) == 1, "a name after clearing"); + Require(X509_VERIFY_PARAM_set1_host_nid_postfix(param, nullptr, 5) == 1 && param->id->hosts == nullptr, "a null name clears the list"); + Require(X509_VERIFY_PARAM_set1_host_nid_postfix(param, nullptr, 0) == 1 && param->id->hosts == nullptr, "clearing an empty list"); + Require(X509_VERIFY_PARAM_set1_host_nid_postfix(param, "last.example", 0) == 1, "a name that the store frees"); + RequireThrows([] { X509_VERIFY_PARAM_set1_host_nid_postfix(nullptr, "a", 0); }, "null verify parameters"); + auto* id = param->id; + param->id = nullptr; + RequireThrows([&] { X509_VERIFY_PARAM_set1_host_nid_postfix(param, "a", 0); }, "verify parameters without an identity"); + param->id = id; + X509_STORE_free_nid_postfix(store); +} + int allocations = 0; int callocs = 0; int frees = 0; int failCallocAt = 0; +int failAllocationAt = 0; void* APS5_VABI Allocate_nid_no_patch(std::size_t size) { - ++allocations; + if (++allocations == failAllocationAt) return nullptr; return std::malloc(size); } @@ -188,6 +227,24 @@ void Allocator() { Require(X509_STORE_new_nid_postfix() == nullptr, "a failed verify parameter allocation fails the store"); Require(frees == allocations + failing - 1, "a failed store releases what it allocated"); } + failCallocAt = 0; + + store = X509_STORE_new_nid_postfix(); + allocations = callocs = frees = 0; + Require(X509_VERIFY_PARAM_set1_host_nid_postfix(store->param, "a.example", 0) == 1 && allocations == 3 && frees == 0, + "a first name allocates its copy, the stack and its array"); + Require(X509_VERIFY_PARAM_set1_host_nid_postfix(store->param, "b.example", 0) == 1 && allocations == 6 && frees == 3, + "a new name frees the old list"); + allocations = frees = 0; + failAllocationAt = 1; + Require(X509_VERIFY_PARAM_set1_host_nid_postfix(store->param, "c.example", 0) == 0 && store->param->id->hosts == nullptr && frees == 3, + "a failed copy leaves the list cleared"); + allocations = frees = 0; + failAllocationAt = 2; + Require(X509_VERIFY_PARAM_set1_host_nid_postfix(store->param, "d.example", 0) == 0 && store->param->id->hosts == nullptr && frees == 1, + "a failed stack frees the copy"); + failAllocationAt = 0; + X509_STORE_free_nid_postfix(store); } } // namespace @@ -204,4 +261,5 @@ int main(int argc, char**) { References(); Identity(); Unsupported(); + Hosts(); } diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index eca9d68a14..29edde6b0e 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -2,7 +2,7 @@ ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 342 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 341 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. - [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); nothing is checked on the console. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw - [DES_set_key, DES_ecb_encrypt, DES_set_odd_parity](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the 128-byte `DES_key_schedule` holds the 8-byte key followed by zeros instead of LibreSSL's precomputed subkeys, so a title reading its contents sees different bytes; `DES_ecb_encrypt` derives the subkeys through mbedtls on each call. Not compared with the console. Results follow LibreSSL [des_key.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des_key.c) and [des.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des.c): `DES_set_key` skips the parity and weak-key checks and returns 0, as it does while `DES_check_key` is 0 (that variable is not in the PS5 export inventory), a nonzero `enc` encrypts, and `DES_set_odd_parity` sets each byte's low bit for odd parity. Null pointers throw where LibreSSL faults - [X509_verify_cert_error_string](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - no console call is verified. The 64-bit argument, low-32-bit lookup, messages and shared numeric fallback follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509/x509_txt.c), the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware versions remain unverified. Unknown-code results share an overwritten static buffer and are not safe for concurrent use, as in that release; code 1 and codes above 54 use the numeric fallback. @@ -19,3 +19,5 @@ - [ASN1_INTEGER_get and ASN1_STRING_data/length/type/to_UTF8](../../../core/libs/prx/libSceLibreSsl/src/Asn1.cpp) are unverified on PS5. Signatures, the 24-byte guest string layout and conversions follow [LibreSSL 2.5.4's ASN.1 sources](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/asn1), the version reported for firmware 13.40 in [discussion #8981](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Null accessor inputs, null conversion outputs, missing nonempty data, oversized converted output and unrepresentable signed integer values throw where the source has undefined behaviour. Malformed encodings and allocation failure throw until ASN.1 error-queue reporting is implemented. Conversion buffers use the guest libc allocator and can be released with guest `free`. - [X509_STORE_new, X509_STORE_free and X509_STORE_set_flags](../../../core/libs/prx/libSceLibreSsl/src/X509Store.cpp) are unverified on PS5. The 136-byte store, 64-byte verify parameters, 56-byte parameter identity, 32-byte stacks and the results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509) `x509_lu.c` and `x509_vpm.c`, the version in [firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). No export adds a certificate, CRL, lookup method or policy object, so `X509_STORE_free` throws instead of freeing one and the object cache's comparator throws. A null store or verify parameters, and a null element array behind a positive count, throw where LibreSSL faults + +- [X509_VERIFY_PARAM_set1_host](../../../core/libs/prx/libSceLibreSsl/src/X509Store.cpp) is unverified on PS5. It follows `int_x509_param_set_hosts` of [LibreSSL 2.7](https://github.com/libressl/openbsd/blob/OPENBSD_6_3/src/lib/libcrypto/x509/x509_vpm.c), the first release with this export, and copies the name with the guest `strndup`. LibreSSL 2.8 also marks the parameters as poisoned when the call fails, through a field the 2.5.4 to 2.7 layout lacks, so a failed call leaves no mark. Null verify parameters or identity throw where LibreSSL faults From 415bc555e728ad3e49e2f8b208ec5b8f539b90eb Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 22 Oct 2026 10:55:34 +0100 Subject: [PATCH 010/160] feat(agc): select the queue worker to sample Port the queue selection from 0be5c7688a onto current main. Keep the process sampler on queue zero and leave the independent committer sampler unchanged. Co-authored-by: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 12 ++++++ .../Execution/include/WorkerSampler.hpp | 5 ++- .../src/Driver/Queues/QueueWorker.cpp | 2 +- .../Execution/src/WorkerSampler.cpp | 12 ++++-- .../libSceAgcDriver/tests/WorkerSampler.cpp | 40 +++++++++++++++++++ docs/dev/ENVIRONMENT.md | 1 + 6 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/WorkerSampler.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 307ccec5dc..ba5397af41 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -534,6 +534,18 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + if(WIN32) + add_test_executable(agc_worker_sampler_tests tests/WorkerSampler.cpp Execution/src/WorkerSampler.cpp) + target_include_directories(agc_worker_sampler_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(agc_worker_sampler_tests PRIVATE libc) + configure_windows_unwind(agc_worker_sampler_tests) + foreach(mode default selected unselected process) + add_test(NAME agc_worker_sampler_${mode} COMMAND agc_worker_sampler_tests ${mode}) + set_tests_properties(agc_worker_sampler_${mode} PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endforeach() + endif() + add_test_executable(agc_stack_walk_tests tests/StackWalk.cpp) target_include_directories(agc_stack_walk_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_stack_walk_tests PRIVATE libc) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp index 4aa7625ba9..57ac083143 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp @@ -1,10 +1,11 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_WORKERSAMPLER_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_WORKERSAMPLER_HPP +#include + namespace AgcDriver { -// Starts sampling the calling thread when APS5_SAMPLE_WORKER names an output file. -void StartWorkerSampler(); +void StartWorkerSampler(std::uint32_t queue = 0); void StartCommitterSampler(); void StopWorkerSampler(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp index 546be83644..09f4c8fc60 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp @@ -141,7 +141,7 @@ void Driver::run(std::uint32_t id) noexcept { PinWorkerThread(role); GuestMemory::TagGpuLockThread(id); - if (id == 0) StartWorkerSampler(); + StartWorkerSampler(id); Submission submission; static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; auto& costs = submissionCosts(id); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp index 3bdac581be..63e53b1483 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp @@ -326,9 +326,13 @@ void StartCallingThreadSampler(const char* path, std::jthread& slot) { } -void StartWorkerSampler() { - StartProcessSampler(); - StartCallingThreadSampler(std::getenv("APS5_SAMPLE_WORKER"), workerSamplerThread); +void StartWorkerSampler(std::uint32_t queue) { + if (queue == 0) StartProcessSampler(); + static const auto sampledQueue = [] { + const char* value = std::getenv("APS5_SAMPLE_WORKER_QUEUE"); + return value != nullptr ? static_cast(std::strtoul(value, nullptr, 0)) : 0u; + }(); + if (queue == sampledQueue) StartCallingThreadSampler(std::getenv("APS5_SAMPLE_WORKER"), workerSamplerThread); } void StartCommitterSampler() { @@ -344,7 +348,7 @@ void StopWorkerSampler() { if (committerSamplerThread.joinable()) committerSamplerThread.join(); } #else -void StartWorkerSampler() {} +void StartWorkerSampler(std::uint32_t) {} void StartCommitterSampler() {} void StopWorkerSampler() {} #endif diff --git a/core/libs/prx/libSceAgcDriver/tests/WorkerSampler.cpp b/core/libs/prx/libSceAgcDriver/tests/WorkerSampler.cpp new file mode 100644 index 0000000000..55d1e6dea8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/WorkerSampler.cpp @@ -0,0 +1,40 @@ +#include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char** argv) { + if (argc != 2) return 1; + const std::string mode = argv[1]; + const bool selected = mode == "selected"; + const bool process = mode == "process"; + const bool workerExpected = selected || mode == "default"; + const auto stem = std::filesystem::temp_directory_path() / ("anyps5-sampler-" + std::to_string(GetCurrentProcessId())); + const auto workerPath = stem.string() + "-worker.txt"; + const auto processPath = stem.string() + "-process.txt"; + _putenv_s("APS5_SAMPLE_WORKER", workerPath.c_str()); + _putenv_s("APS5_SAMPLE_THREADS", process ? processPath.c_str() : ""); + _putenv_s("APS5_SAMPLE_WORKER_QUEUE", mode == "default" ? "" : "0x50"); + AgcDriver::StartWorkerSampler(selected ? 0x50 : 0); + const auto until = std::chrono::steady_clock::now() + std::chrono::milliseconds(200); + while (std::chrono::steady_clock::now() < until) std::this_thread::yield(); + AgcDriver::StopWorkerSampler(); + const auto sampled = [](const std::string& path) { + std::ifstream file(path); + std::string marker; + std::uint64_t samples = 0; + return (file >> marker >> samples) && marker == "#" && samples != 0; + }; + const bool passed = sampled(workerPath) == workerExpected && sampled(processPath) == process; + std::filesystem::remove(workerPath); + std::filesystem::remove(processPath); + std::filesystem::remove(stem.string() + "-worker.collapsed.txt"); + std::filesystem::remove(stem.string() + "-worker.collapsed.txt.tmp"); + std::printf("worker sampler %s: %s\n", mode.c_str(), passed ? "passed" : "failed"); + return passed ? 0 : 1; +} diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 7b29095640..4dd0edc08a 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -377,6 +377,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_SAMPLE_COMMITTER` | string | `core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp` | | samples queue 0's draw committer every millisecond and writes its stacks to a file (Windows only) | | `APS5_SAMPLE_THREADS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp` | | samples every thread of the process every 2 ms and writes each busy thread's hottest frames to a file (Windows only) | | `APS5_SAMPLE_WORKER` | string | `core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp` | | samples the calling queue worker every millisecond and writes its stacks to a file (Windows only) | +| `APS5_SAMPLE_WORKER_QUEUE` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp` | 0 | selects the queue worker sampled by `APS5_SAMPLE_WORKER`, accepting decimal or hexadecimal IDs such as `0x50`; process and committer sampling remain independent | | `APS5_SHADOW_VERIFY` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp` | | verifies that the shadow-published bytes equal the guest bytes | | `APS5_SHARED_COLLECT_MEMO` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | uses the tracker's shared collect-memo ring under its mutex instead of the per-thread ring | | `APS5_SHARED_MEMORY_COUNTERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | accumulates the memory counters into one shared set with atomic RMWs | From 10466c30e61408e2469a649fd48387d02533ae62 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 26 Oct 2026 15:24:33 +1300 Subject: [PATCH 011/160] feat(shader): carry a blended normalized color target merge in the pixel request TargetMerge gains a TargetBlend (factors, operations, write mask, constants, signedness) that the request serializer, the cache key and the variant hash take only when it is enabled, so every request without it keeps its bytes, key and hash. --- core/shader/recompiler/CacheKey.hpp | 12 ++++++ .../ControlFlow/src/RequestSerializer.cpp | 37 ++++++++++++++++--- .../Optimization/ShaderStageInputInfo.hpp | 1 + core/shader/recompiler/Recompiler.cpp | 11 ++++++ core/shader/recompiler/Recompiler.hpp | 16 ++++++++ .../src/ShaderInputInfoBuilder.cpp | 3 ++ 6 files changed, 75 insertions(+), 5 deletions(-) diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 8c5d2b2678..939030401f 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -225,6 +225,18 @@ private: append(key, value.targetMerge.samples); append(key, value.targetMerge.depthStencilWrites); append(key, value.targetMerge.depthStencilTests); + if (value.targetMerge.blend.enabled) { + const auto& blend = value.targetMerge.blend; + append(key, blend.snorm); + append(key, blend.colorSource); + append(key, blend.colorDestination); + append(key, blend.colorOperation); + append(key, blend.alphaSource); + append(key, blend.alphaDestination); + append(key, blend.alphaOperation); + append(key, blend.writeMask); + append(key, blend.constants); + } append(key, value.sampleOrder); } diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 1a12c1702c..ab2e1f133b 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -6,6 +6,8 @@ namespace ShaderRecompiler { namespace { +constexpr std::uint32_t TargetBlendFlag = 0x10000u; + constexpr char kBase64Alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; std::string base64Encode(std::string_view data) { @@ -358,11 +360,23 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { writer.WriteBool(info.stencilOperationValueExport); for (const std::uint32_t order : info.sampleOrder) writer.WriteU32(order); writer.WriteU8(info.targetMerge.target); - writer.WriteU32(info.targetMerge.mask); + writer.WriteU32(static_cast(info.targetMerge.mask) | (info.targetMerge.blend.enabled ? TargetBlendFlag : 0u)); writer.WriteU8(info.targetMerge.samples); for (const std::uint32_t word : info.targetMerge.descriptor) writer.WriteU32(word); writer.WriteBool(info.targetMerge.depthStencilWrites); writer.WriteBool(info.targetMerge.depthStencilTests); + if (info.targetMerge.blend.enabled) { + const auto& blend = info.targetMerge.blend; + writer.WriteBool(blend.snorm); + writer.WriteU8(blend.colorSource); + writer.WriteU8(blend.colorDestination); + writer.WriteU8(blend.colorOperation); + writer.WriteU8(blend.alphaSource); + writer.WriteU8(blend.alphaDestination); + writer.WriteU8(blend.alphaOperation); + writer.WriteU8(blend.writeMask); + for (const std::uint32_t word : blend.constants) writer.WriteU32(word); + } } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -436,13 +450,26 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { } if (version >= 46u) { info.targetMerge.target = reader.ReadU8(); - const auto mask = reader.ReadU32(); - if (mask > 0xffffu) throw std::runtime_error("invalid recompile request target merge mask"); - info.targetMerge.mask = static_cast(mask); + const auto word = reader.ReadU32(); + if ((word & ~(0xffffu | TargetBlendFlag)) != 0u) throw std::runtime_error("invalid recompile request target merge mask"); + info.targetMerge.mask = static_cast(word & 0xffffu); info.targetMerge.samples = reader.ReadU8(); - for (std::uint32_t& word : info.targetMerge.descriptor) word = reader.ReadU32(); + for (std::uint32_t& descriptor : info.targetMerge.descriptor) descriptor = reader.ReadU32(); info.targetMerge.depthStencilWrites = reader.ReadBool(); info.targetMerge.depthStencilTests = reader.ReadBool(); + if ((word & TargetBlendFlag) != 0u) { + auto& blend = info.targetMerge.blend; + blend.enabled = true; + blend.snorm = reader.ReadBool(); + blend.colorSource = reader.ReadU8(); + blend.colorDestination = reader.ReadU8(); + blend.colorOperation = reader.ReadU8(); + blend.alphaSource = reader.ReadU8(); + blend.alphaDestination = reader.ReadU8(); + blend.alphaOperation = reader.ReadU8(); + blend.writeMask = reader.ReadU8(); + for (std::uint32_t& constant : blend.constants) constant = reader.ReadU32(); + } } if (version < 5u) { const auto input = [](PixelInput value, bool present) { return present ? PixelInputBit(value) : 0u; }; diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 299b838494..736cbb5ff0 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -204,6 +204,7 @@ struct ShaderPixelInputInfo { std::uint8_t mergeSamples = 1; bool mergeDepthStencilWrites = false; bool mergeDepthStencilTests = false; + TargetBlend mergeBlend{}; std::array sampleOrder{}; ShaderStageRuntime stage; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 3aa81cb5d7..4e040347ad 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -1733,6 +1733,17 @@ std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapsh mix(merge.mask); mix(merge.samples); for (const auto word : merge.descriptor) mix(word); + if (merge.blend.enabled) { + mix(merge.blend.snorm); + mix(merge.blend.colorSource); + mix(merge.blend.colorDestination); + mix(merge.blend.colorOperation); + mix(merge.blend.alphaSource); + mix(merge.blend.alphaDestination); + mix(merge.blend.alphaOperation); + mix(merge.blend.writeMask); + for (const auto word : merge.blend.constants) mix(word); + } } if (request.context.vertex) { const auto& vertex = *request.context.vertex; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 6d3cc50544..efa733b500 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -166,6 +166,21 @@ inline constexpr std::uint32_t One = 3u; } +struct TargetBlend { + bool enabled = false; + bool snorm = false; + std::uint8_t colorSource = 0; + std::uint8_t colorDestination = 0; + std::uint8_t colorOperation = 0; + std::uint8_t alphaSource = 0; + std::uint8_t alphaDestination = 0; + std::uint8_t alphaOperation = 0; + std::uint8_t writeMask = 0; + std::array constants{}; + + bool operator==(const TargetBlend&) const = default; +}; + struct TargetMerge { std::uint8_t target = 0; std::uint16_t mask = 0; @@ -173,6 +188,7 @@ struct TargetMerge { std::array descriptor{}; bool depthStencilWrites = false; bool depthStencilTests = false; + TargetBlend blend{}; bool operator==(const TargetMerge&) const = default; }; diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 23ec115894..7b3b24c605 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -173,6 +173,9 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.mergeSamples = pixel.targetMerge.samples; pixelStorage.mergeDepthStencilWrites = pixel.targetMerge.depthStencilWrites; pixelStorage.mergeDepthStencilTests = pixel.targetMerge.depthStencilTests; + const auto& blend = pixel.targetMerge.blend; + if (blend.enabled && (pixel.targetMerge.mask == 0u || pixel.targetMerge.samples != 1u || blend.colorSource > 14u || blend.colorDestination > 14u || blend.alphaSource > 14u || blend.alphaDestination > 14u || blend.colorOperation > 4u || blend.alphaOperation > 4u || blend.writeMask > 0xfu)) throw std::runtime_error("ShaderInputInfoBuilder: invalid color target blend"); + pixelStorage.mergeBlend = blend; pixelStorage.sampleOrder = pixel.sampleOrder; ShaderStageInputInfo result; result.pixel = &pixelStorage; From 3d09149cd8057e6b9e8a5a8c4aeabb8ba0172d79 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 26 Oct 2026 15:24:33 +1300 Subject: [PATCH 012/160] feat(shader): blend an unclamped normalized color target in the pixel shader With a target blend in the merge, the pixel shader reads the target through its storage image under pixel interlock, evaluates the blend equation on the unclamped export, clamps and stores the result. It reuses the packed 4_4_4_4 partial write merge and its restrictions. --- .../Optimization/src/ShaderInfoCollector.cpp | 2 +- .../SpirvBackend/SpirvEmitterState.hpp | 1 + .../src/SpirvMemory/SpirvModuleSetup.cpp | 7 +- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 101 +++++++++++++++++- 4 files changed, 103 insertions(+), 8 deletions(-) diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index 472ade83d6..f9cd76bf81 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -563,7 +563,7 @@ void AddTargetMerge(const ShaderPixelInputInfo& pixel, ShaderInfo& info) { ImageResource image; image.source = std::numeric_limits::max(); image.resourceClass = ImageResourceClass::Storage; - image.numericClass = IrTextureNumericClass::Uint; + image.numericClass = pixel.mergeBlend.enabled ? IrTextureNumericClass::Float : IrTextureNumericClass::Uint; image.dimension = pixel.mergeSamples > 1u ? RdnaImageDimension::Dim2DMsaa : RdnaImageDimension::Dim2D; image.read = true; image.written = true; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index fda431fc05..a2ddb3889f 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -235,6 +235,7 @@ struct SpirvEmitterState { const IrBlock* currentBlock = nullptr; std::uint32_t pixelValidMaskVariable = 0; std::uint32_t mergeWordVariable = 0; + std::uint32_t mergeTexelVariable = 0; std::uint32_t mergePendingVariable = 0; std::uint32_t quadPixelUncovered = 0; std::uint32_t sampleMaskInputVariable = 0; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index cab4d0b74c..899b116196 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -411,21 +411,22 @@ void DefineModule(SpirvEmitterState& state) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeStencilRefReplacingEXT); } const bool merge = pixel.mergeMask != 0u; + const std::string mergeName = pixel.mergeBlend.enabled ? "blending into an unclamped normalized color target" : "a partial write of a packed color target"; if (merge && (state.depthVariable != 0 || state.stencilRefVariable != 0 || pixel.psDepthExportEnable || pixel.psSampleMaskExportEnable || pixel.psStencilRefExportEnable || pixel.psAlphaToCoverage || pixel.QuadMasked() || (pixel.psPixelKillEnable && !pixel.psEarlyZ && pixel.mergeDepthStencilWrites))) { - throw std::runtime_error("a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that kills pixels under late tests of a draw writing depth or stencil, exports depth, stencil or coverage, or runs with a quad sample mask cannot use"); + throw std::runtime_error(mergeName + " merges inside the pixel shader after early depth and stencil tests, which a shader that kills pixels under late tests of a draw writing depth or stencil, exports depth, stencil or coverage, or runs with a quad sample mask cannot use"); } if (merge && pixel.mergeSamples > 1u && WritesGuestMemory(state.program)) { throw std::runtime_error("a partial write of a multisampled packed color target runs the pixel shader once per sample, which a shader that writes guest memory (stores, atomics or GDS) cannot use: each covered sample would repeat its writes"); } if (merge && !pixel.psEarlyZ && pixel.mergeDepthStencilTests && WritesGuestMemory(state.program)) { - throw std::runtime_error("a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that writes guest memory (stores, atomics or GDS) under late tests cannot use: the pixels the tests reject would still write it"); + throw std::runtime_error(mergeName + " merges inside the pixel shader after early depth and stencil tests, which a shader that writes guest memory (stores, atomics or GDS) under late tests cannot use: the pixels the tests reject would still write it"); } if ((pixel.psEarlyZ && !pixel.psAlphaToCoverage && !pixel.QuadMasked()) || merge) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests); } if (pixel.psOrderedPixelShader || merge) { if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityFragmentShaderPixelInterlockEXT)) == state.supportedCapabilities.end()) { - throw std::runtime_error(merge ? "a partial write of a packed color target needs the fragmentShaderPixelInterlock feature, which the device lacks" : "a primitive-ordered pixel shader needs the fragmentShaderPixelInterlock feature, which the device lacks"); + throw std::runtime_error(merge ? mergeName + " needs the fragmentShaderPixelInterlock feature, which the device lacks" : "a primitive-ordered pixel shader needs the fragmentShaderPixelInterlock feature, which the device lacks"); } state.module.EmitCapability(spv::CapabilityFragmentShaderPixelInterlockEXT); state.module.EmitExtension("SPV_EXT_fragment_shader_interlock"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index e12ee79af6..050fc909fe 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -473,7 +473,7 @@ MergeTexel MergeTargetTexel(SpirvEmitterState& state) { const auto image = std::ranges::find_if(images, [](const ImageResource& candidate) { return candidate.targetMerge; }); if (image == images.end()) FailEmit("the color target merge image was not collected"); if (InputVariableForKind(state, StageInputKind::FragCoord) == 0u || (pixel.mergeSamples > 1u && InputVariableForKind(state, StageInputKind::SampleId) == 0u)) FailEmit("the color target merge inputs were not declared"); - if (state.mergeWordVariable == 0u || state.mergePendingVariable == 0u) FailEmit("the color target merge variables were not declared"); + if ((state.mergeWordVariable == 0u && state.mergeTexelVariable == 0u) || state.mergePendingVariable == 0u) FailEmit("the color target merge variables were not declared"); const auto position = [&](std::uint32_t component) { return Unary(state, spv::OpConvertFToU, TypeU32(state), Unary(state, spv::OpBitcast, TypeF32(state), EmitBuiltinU32(state, StageInputKind::FragCoord, component))); }; const auto x = position(0u); const auto y = position(1u); @@ -482,8 +482,91 @@ MergeTexel MergeTargetTexel(SpirvEmitterState& state) { return {static_cast(image - images.begin()), coord, pixel.mergeSamples > 1u ? EmitBuiltinU32(state, StageInputKind::SampleId, 0u) : 0u}; } +std::uint32_t BlendComponent(SpirvEmitterState& state, std::uint32_t vector, std::uint32_t index) { + const auto component = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), component, vector, index); + return component; +} + +std::uint32_t BlendArithmetic(SpirvEmitterState& state, spv::Op operation, std::uint32_t left, std::uint32_t right) { + const auto result = Binary(state, operation, TypeF32(state), left, right); + state.module.AddAnnotation(spv::OpDecorate, result, spv::DecorationNoContraction); + return result; +} + +std::uint32_t BlendGlsl(SpirvEmitterState& state, GLSLstd450 operation, std::uint32_t first, std::uint32_t second) { + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), result, GlslStd450(state), operation, first, second); + return result; +} + +std::uint32_t BlendFactor(SpirvEmitterState& state, std::uint32_t kind, std::uint32_t channel, bool alpha, const std::array& source, const std::array& destination) { + const auto& blend = PixelInfo(state).mergeBlend; + const auto one = ConstantF32(state, std::bit_cast(1.0f)); + const auto complement = [&](std::uint32_t value) { return BlendArithmetic(state, spv::OpFSub, one, value); }; + const std::uint32_t index = alpha ? 3u : channel; + switch (kind) { + case 0u: return ConstantF32(state, std::bit_cast(0.0f)); + case 1u: return one; + case 2u: return source[index]; + case 3u: return complement(source[index]); + case 4u: return destination[index]; + case 5u: return complement(destination[index]); + case 6u: return source[3]; + case 7u: return complement(source[3]); + case 8u: return destination[3]; + case 9u: return complement(destination[3]); + case 10u: return ConstantF32(state, blend.constants[index]); + case 11u: return complement(ConstantF32(state, blend.constants[index])); + case 12u: return ConstantF32(state, blend.constants[3]); + case 13u: return complement(ConstantF32(state, blend.constants[3])); + case 14u: return alpha ? one : BlendGlsl(state, GLSLstd450FMin, source[3], complement(destination[3])); + default: FailEmit("a color target blend names an unsupported blend factor"); + } +} + +std::uint32_t BlendEquation(SpirvEmitterState& state, std::uint32_t operation, std::uint32_t sourceFactor, std::uint32_t destinationFactor, std::uint32_t source, std::uint32_t destination) { + switch (operation) { + case 0u: return BlendArithmetic(state, spv::OpFAdd, BlendArithmetic(state, spv::OpFMul, source, sourceFactor), BlendArithmetic(state, spv::OpFMul, destination, destinationFactor)); + case 1u: return BlendArithmetic(state, spv::OpFSub, BlendArithmetic(state, spv::OpFMul, source, sourceFactor), BlendArithmetic(state, spv::OpFMul, destination, destinationFactor)); + case 2u: return BlendArithmetic(state, spv::OpFSub, BlendArithmetic(state, spv::OpFMul, destination, destinationFactor), BlendArithmetic(state, spv::OpFMul, source, sourceFactor)); + case 3u: return BlendGlsl(state, GLSLstd450FMin, source, destination); + case 4u: return BlendGlsl(state, GLSLstd450FMax, source, destination); + default: FailEmit("a color target blend names an unsupported blend operation"); + } +} + +void MergeBlendTarget(SpirvEmitterState& state, std::uint32_t color) { + const auto& blend = PixelInfo(state).mergeBlend; + const auto [resource, coord, sample] = MergeTargetTexel(state); + const auto stored = EmitStorageImageRead(state, resource, coord, sample); + std::array source{}; + std::array destination{}; + for (std::uint32_t channel = 0; channel < 4u; ++channel) { + source[channel] = BlendComponent(state, color, channel); + destination[channel] = BlendComponent(state, stored, channel); + } + const auto low = ConstantF32(state, std::bit_cast(blend.snorm ? -1.0f : 0.0f)); + const auto high = ConstantF32(state, std::bit_cast(1.0f)); + std::array result{}; + for (std::uint32_t channel = 0; channel < 4u; ++channel) { + const bool alpha = channel == 3u; + const auto sourceFactor = BlendFactor(state, alpha ? blend.alphaSource : blend.colorSource, channel, alpha, source, destination); + const auto destinationFactor = BlendFactor(state, alpha ? blend.alphaDestination : blend.colorDestination, channel, alpha, source, destination); + const auto value = BlendEquation(state, alpha ? blend.alphaOperation : blend.colorOperation, sourceFactor, destinationFactor, source[channel], destination[channel]); + const auto clamped = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), clamped, GlslStd450(state), GLSLstd450NClamp, value, low, high); + result[channel] = ((blend.writeMask >> channel) & 1u) != 0u ? clamped : destination[channel]; + } + const auto merged = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 4u), merged, result[0], result[1], result[2], result[3]); + state.module.AddFunction(spv::OpStore, state.mergeTexelVariable, merged); + state.module.AddFunction(spv::OpStore, state.mergePendingVariable, ConstantU32(state, 1u)); +} + void MergeColorTarget(SpirvEmitterState& state, std::uint32_t color) { const auto& pixel = PixelInfo(state); + if (pixel.mergeBlend.enabled) return MergeBlendTarget(state, color); const auto [resource, coord, sample] = MergeTargetTexel(state); const auto stored = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), stored, EmitStorageImageRead(state, resource, coord, sample), 0u); @@ -820,6 +903,12 @@ void FlushColorTargetMerge(SpirvEmitterState& state) { } EmitIfCondition(state, write, [&] { const auto [resource, coord, sample] = MergeTargetTexel(state); + if (state.mergeTexelVariable != 0u) { + const auto blended = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeF32Vector(state, 4u), blended, state.mergeTexelVariable); + EmitStorageImageWrite(state, resource, 0u, coord, blended, sample); + return; + } const auto merged = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, TypeU32(state), merged, state.mergeWordVariable); const auto texel = state.module.AllocateId(); @@ -1583,7 +1672,7 @@ void EmitProgram(SpirvEmitterState& state) { state.module.AddName(state.pixelValidMaskVariable, "pixel_valid_mask_active"); } if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->mergeMask != 0u) { - state.mergeWordVariable = state.module.AllocateId(); + (state.inputInfo.pixel->mergeBlend.enabled ? state.mergeTexelVariable : state.mergeWordVariable) = state.module.AllocateId(); state.mergePendingVariable = state.module.AllocateId(); } for (const IrBlock* block : program.BlockOrder()) { @@ -1636,7 +1725,11 @@ void EmitProgram(SpirvEmitterState& state) { state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), state.pixelValidMaskVariable, spv::StorageClassFunction); } if (state.mergePendingVariable != 0u) { - state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), state.mergeWordVariable, spv::StorageClassFunction); + if (state.mergeTexelVariable != 0u) { + state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, TypeF32Vector(state, 4u)), state.mergeTexelVariable, spv::StorageClassFunction); + } else { + state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), state.mergeWordVariable, spv::StorageClassFunction); + } state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), state.mergePendingVariable, spv::StorageClassFunction, ConstantU32(state, 0u)); } for (std::uint32_t half = 0; half < state.laneCount; half++) { @@ -2024,7 +2117,7 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { if (exp.index == 0u && !uintOutput && AlphaToCoverage(state)) StoreAlphaCoverage(state, value); const auto mode = MrtOutputMode(state, exp); if (PixelInfo(state).mergeMask != 0u && exp.index == PixelInfo(state).mergeTarget) { - if (uintOutput || mode == 5u || mode == 6u) FailEmit("a partial write of a packed color target cannot take an integer or 16-bit normalized export"); + if (uintOutput || mode == 5u || mode == 6u) FailEmit(std::string(PixelInfo(state).mergeBlend.enabled ? "blending into an unclamped normalized color target" : "a partial write of a packed color target") + " cannot take an integer or 16-bit normalized export"); MergeColorTarget(state, value); } if (!uintOutput && mode != 5u && mode != 6u) value = SelectExportPacking(state, exp.index, value); From d3f0556483c6b26a5e5db0c1b0c353dc3e58c403 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 26 Oct 2026 15:24:33 +1300 Subject: [PATCH 013/160] feat(agc): route blending into an unclamped normalized color target to the pixel shader A UNORM or SNORM target with BLEND_CLAMP clear and blending enabled was refused. It is now merged in the pixel shader, since Vulkan clamps every blend input to the normalized range. Dual-source, multisampled, packed, reversed and alternate component order targets are still refused. --- .../Execution/src/Driver/Draw/DrawLookup.cpp | 12 ++++++++++ .../Graphics/include/State.hpp | 2 ++ .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 7 ++++++ .../libSceAgcDriver/Graphics/src/State.cpp | 24 ++++++++++++++++++- 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index ec65e9734e..bf99d63bc2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -85,6 +85,18 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(pixel.rasterPrimitive)); mix(pixel.ditheredAlphaToCoverage); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 13d227b8ea..5d944881e5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -34,6 +34,8 @@ struct ShaderStages { float maxTessellationLevel = 64.0f; }; +inline constexpr std::uint16_t UnclampedBlendMask = 0xffffu; + struct ColorTarget { std::uint64_t address; VkExtent2D extent; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 1dd1e6c450..28d7a96c7c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -666,6 +666,13 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput const bool alternateAlpha = SplitsAlternateAlpha(state); std::vector firstChannels(state.blends.begin(), state.blends.end()); std::vector secondChannels(state.blends.begin(), state.blends.end()); + for (const auto& merged : state.colors) { + if (merged.partialWriteMask != UnclampedBlendMask) continue; + for (auto* channels : {&firstChannels, &secondChannels}) { + channels->at(merged.exportIndex).blendEnable = VK_FALSE; + channels->at(merged.exportIndex).colorWriteMask = 0; + } + } if (alternateAlpha) { const auto keepsStencil = [](const VkStencilOpState& face) { return face.writeMask == 0 || (face.failOp == VK_STENCIL_OP_KEEP && face.passOp == VK_STENCIL_OP_KEEP && face.depthFailOp == VK_STENCIL_OP_KEEP); }; Require(!(state.depth && (state.depthWrite || (state.stencilTest && (!keepsStencil(state.stencilFront) || !keepsStencil(state.stencilBack))))) && !state.depthBiasPerFace, "blending into an 8_8 color target with the alternate component order and a separate alpha blend from a draw that writes depth or stencil is not implemented"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 3c280c1312..5ef6146d39 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1785,7 +1785,11 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { if (const auto info = read(cx, 0x31c + slot * 0xfu); state.blendEnable && (info & 0x8000u) == 0 && ((info >> 8u) & 7u) <= 1u) { char detail[80]; std::snprintf(detail, sizeof(detail), " (CB_COLOR_INFO 0x%08x, CB_BLEND%u_CONTROL 0x%08x)", info, slot, blend); - throw std::runtime_error(std::string("AGC graphics: blending unclamped normalized color is not implemented") + detail); + const std::string where = detail; + Require(color.samples == 1u && color.packing == ShaderRecompiler::ColorExportPacking::None, "blending unclamped normalized color into a multisampled or packed color target is not implemented" + where); + Require(!reversed && mapping != 0xecu, "blending unclamped normalized color into a color target with a reversed or alternate component order is not implemented" + where); + Require(!(secondSource(state.srcColorBlendFactor) || secondSource(state.dstColorBlendFactor) || secondSource(state.srcAlphaBlendFactor) || secondSource(state.dstAlphaBlendFactor)), "dual-source blending of unclamped normalized color is not implemented" + where); + color.partialWriteMask = UnclampedBlendMask; } result.blends[color.exportIndex] = state; } @@ -1854,7 +1858,12 @@ bool DepthStencilTests(const State& state) { return state.depth.has_value() && (state.depthTest || state.stencilTest || state.depthBoundsTest); } +static bool SignedNormalizedFormat(VkFormat format) { + return format == VK_FORMAT_R8_SNORM || format == VK_FORMAT_R16_SNORM || format == VK_FORMAT_R8G8_SNORM || format == VK_FORMAT_R16G16_SNORM || format == VK_FORMAT_R8G8B8A8_SNORM || format == VK_FORMAT_B8G8R8A8_SNORM || format == VK_FORMAT_R16G16B16A16_SNORM; +} + ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state) { + static_assert(VK_BLEND_FACTOR_ZERO == 0 && VK_BLEND_FACTOR_SRC_ALPHA_SATURATE == 14 && VK_BLEND_OP_ADD == 0 && VK_BLEND_OP_MAX == 4); ShaderRecompiler::TargetMerge merge; for (const auto& color : state.colors) { if (color.partialWriteMask == 0) continue; @@ -1866,6 +1875,19 @@ ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state) { merge.descriptor = EncodeTextureResource(TargetMergeSurface(color)); merge.depthStencilWrites = DepthStencilWrites(state); merge.depthStencilTests = DepthStencilTests(state); + if (color.partialWriteMask != UnclampedBlendMask) continue; + const auto& attachment = state.blends.at(color.exportIndex); + auto& blend = merge.blend; + blend.enabled = true; + blend.snorm = SignedNormalizedFormat(color.format); + blend.colorSource = static_cast(attachment.srcColorBlendFactor); + blend.colorDestination = static_cast(attachment.dstColorBlendFactor); + blend.colorOperation = static_cast(attachment.colorBlendOp); + blend.alphaSource = static_cast(attachment.srcAlphaBlendFactor); + blend.alphaDestination = static_cast(attachment.dstAlphaBlendFactor); + blend.alphaOperation = static_cast(attachment.alphaBlendOp); + blend.writeMask = static_cast(attachment.colorWriteMask & 0xfu); + for (std::size_t i = 0; i < 4; ++i) blend.constants[i] = std::bit_cast(state.blendConstants[i]); } return merge; } From 7884b6c85fcc3f1824ab219024ad1779ac47b9dd Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 26 Oct 2026 15:24:33 +1300 Subject: [PATCH 014/160] test(agc): check unclamped normalized color blending against a float oracle --- .../prx/libSceAgcDriver/tests/Graphics.cpp | 10 +- .../execution/UnclampedNormalizedBlend.cpp | 486 ++++++++++++++++++ 2 files changed, 494 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/UnclampedNormalizedBlend.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index e1896d291f..4e3d552e99 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1782,14 +1782,20 @@ void UnclampedNormalizedColorTests() { const auto pass = AgcDriver::Graphics::DecodeColorMetadataPass(eliminate); Require(pass.has_value() && pass->mode == AgcDriver::Graphics::ColorMetadataPass::Mode::EliminateFastClear && pass->targets.size() == 1 && pass->targets[0].format == VK_FORMAT_R8G8_UNORM, "a fast-clear eliminate over an unclamped UNORM target was refused"); queue.context[0x1e0] = blending; - expectFailure([&] { DecodeState(queue); }, "blending unclamped normalized color is not implemented (CB_COLOR_INFO 0x0000000c, CB_BLEND0_CONTROL 0x40000504)"); + state = DecodeState(queue); + Require(state.colors.at(0).partialWriteMask == AgcDriver::Graphics::UnclampedBlendMask && state.blends.at(0).blendEnable && state.blends.at(0).colorWriteMask != 0, "an 8_8 UNORM target that blends with BLEND_CLAMP clear (CB_COLOR_INFO 0x0000000c, CB_BLEND0_CONTROL 0x40000504) was not routed to the pixel shader"); + const auto merge = AgcDriver::Graphics::ExportTargetMerge(state); + Require(merge.blend.enabled && !merge.blend.snorm && merge.mask == AgcDriver::Graphics::UnclampedBlendMask && merge.samples == 1u && merge.blend.colorSource == VK_BLEND_FACTOR_SRC_ALPHA && merge.blend.colorDestination == VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA && merge.blend.colorOperation == VK_BLEND_OP_ADD && merge.blend.writeMask == state.blends.at(0).colorWriteMask, "the unclamped blend of an 8_8 UNORM target was not described to the pixel shader"); queue.context[0x31c] |= 0x10000u; state = DecodeState(queue); Require(!state.blends.at(0).blendEnable && state.blends.at(0).colorWriteMask != 0, "an unclamped UNORM target with BLEND_BYPASS was refused or blended"); queue = target(0x0000010cu); Require(DecodeState(queue).colors.at(0).format == VK_FORMAT_R8G8_SNORM, "an unclamped SNORM target that does not blend was refused"); queue.context[0x1e0] = blending; - expectFailure([&] { DecodeState(queue); }, "blending unclamped normalized color is not implemented"); + state = DecodeState(queue); + Require(state.colors.at(0).partialWriteMask == AgcDriver::Graphics::UnclampedBlendMask && AgcDriver::Graphics::ExportTargetMerge(state).blend.snorm, "an unclamped SNORM target that blends was not routed to the pixel shader as signed"); + queue.context[0x1e0] = (1u << 30u) | 15u | (15u << 8u); + expectFailure([&] { DecodeState(queue); }, "dual-source blending of unclamped normalized color is not implemented (CB_COLOR_INFO 0x0000010c, CB_BLEND0_CONTROL 0x40000f0f)"); queue = target(0x00000604u); expectFailure([&] { DecodeState(queue); }, "unclamped normalized color is unsupported"); queue = target(0x0000800cu); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/UnclampedNormalizedBlend.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/UnclampedNormalizedBlend.cpp new file mode 100644 index 0000000000..3b4498ca6e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/UnclampedNormalizedBlend.cpp @@ -0,0 +1,486 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 256; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t ScissorWidth = 128; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Unorm8888Info = 10u << 2u; +constexpr std::uint32_t Snorm8888Info = (10u << 2u) | (1u << 8u); +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t PositionInputs = 0x300u; +constexpr std::uint32_t DepthInput = 0x400u; + + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 64> Programs{}; +std::size_t programCount = 0; + +std::span Float32Program(std::array rgba) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + for (std::uint32_t vgpr = 0; vgpr < 4u; ++vgpr) { + code[at++] = 0x7e0002ffu | ((4u + vgpr) << 17u); + code[at++] = std::bit_cast(rgba[vgpr]); + } + code[at++] = 0xf800180fu; + code[at++] = 0x07060504u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +std::span DepthAsXProgram(std::array rgba) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + code[at++] = 0x7e080300u; + for (std::uint32_t vgpr = 1; vgpr < 4u; ++vgpr) { + code[at++] = 0x7e0002ffu | ((4u + vgpr) << 17u); + code[at++] = std::bit_cast(rgba[vgpr]); + } + code[at++] = 0xf800180fu; + code[at++] = 0x07060504u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +const std::array, 3>& TriangleAt(float z) { + static std::map, 3>> triangles; + return triangles.try_emplace(z, std::array, 3>{{{-1.0f, -1.0f, z, 1.0f}, {3.0f, -1.0f, z, 1.0f}, {-1.0f, 3.0f, z, 1.0f}}}).first->second; +} + +struct DrawSetup { + std::uint32_t inputs = 0; + std::uint32_t shaderControl = 0; + std::span> vertices = Triangle; + std::uint64_t depth = 0; + bool depthWrite = false; +}; + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +struct BlendCase { + VkBlendFactor colorSource; + VkBlendFactor colorDestination; + VkBlendOp colorOperation; + VkBlendFactor alphaSource; + VkBlendFactor alphaDestination; + VkBlendOp alphaOperation; + VkColorComponentFlags writeMask; + std::array constants; + bool snorm; +}; + + +void Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTarget color, const BlendCase& blendCase, std::uint32_t exportFormat, std::span pixelCode, const DrawSetup& setup = {}) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(setup.vertices.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(setup.vertices.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + color.partialWriteMask = AgcDriver::Graphics::UnclampedBlendMask; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {ScissorWidth, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + VkPipelineColorBlendAttachmentState blend{}; + blend.blendEnable = VK_TRUE; + blend.srcColorBlendFactor = blendCase.colorSource; + blend.dstColorBlendFactor = blendCase.colorDestination; + blend.colorBlendOp = blendCase.colorOperation; + blend.srcAlphaBlendFactor = blendCase.alphaSource; + blend.dstAlphaBlendFactor = blendCase.alphaDestination; + blend.alphaBlendOp = blendCase.alphaOperation; + blend.colorWriteMask = blendCase.writeMask; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = blendCase.constants; + if (setup.depth != 0) { + state.depth = AgcDriver::Graphics::DepthTarget{setup.depth, 0, {Width, Height}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0, false}; + state.depthTest = true; + state.depthWrite = setup.depthWrite; + state.depthCompare = VK_COMPARE_OP_LESS; + } + + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, setup.inputs}, {0x1b4, setup.inputs}, {0x203, setup.shaderControl}, {0x1c5, exportFormat}}; + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + const auto handle = ShaderRecompiler::PrepareShader(fragment); + AgcDriver::DriverDetail::ShaderSnapshot snapshot{codeAddress, 0, 1, {pixelCode.begin(), pixelCode.end()}, {}}; + snapshot.prepared->entries.push_back({0, handle}); + fragment.shader.code = snapshot.code; + fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + fragment.context.pixel->targetExportPacking = AgcDriver::Graphics::ExportPackings(state); + fragment.context.pixel->targetMerge = AgcDriver::Graphics::ExportTargetMerge(state); + Require(!fragment.context.pixel->pixelKillEnable && !fragment.context.pixel->earlyZ, "the test pixel shader does not kill"); + Require(fragment.context.pixel->targetMerge.blend.enabled && fragment.context.pixel->targetMerge.mask == AgcDriver::Graphics::UnclampedBlendMask && fragment.context.pixel->targetMerge.blend.snorm == blendCase.snorm, "the draw's target merge does not describe the unclamped blend"); + const ShaderRecompiler::SrtRuntime runtime{}; + const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, fragment); + const auto pixelResult = invocation.Materialize(*invocation.Capture(runtime)); + const bool registered = pixelResult->variantId == ShaderRecompiler::GetPreparedArtifact(*handle).variantId; + Require(!registered, "the blending draw reused the registered pixel shader"); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(setup.vertices.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} +struct Case { + const char* name; + BlendCase blend; + std::array rgba; + bool distinguishesClamping; +}; + +constexpr int Tolerance = 1; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, bool snorm) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = snorm ? Snorm8888Info : Unorm8888Info; + cx[0x31d] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = TiledAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == (snorm ? VK_FORMAT_R8G8B8A8_SNORM : VK_FORMAT_R8G8B8A8_UNORM) && color.componentMapping == 0xe4u && color.packing == ShaderRecompiler::ColorExportPacking::None && color.tileMode == ColorTileMode::RenderTarget && color.bytes == block.bytes, "the 8_8_8_8 target did not decode as expected"); + return color; +} + +float Factor(VkBlendFactor kind, std::uint32_t channel, bool alpha, const std::array& source, const std::array& destination, const std::array& constants) { + const std::uint32_t index = alpha ? 3u : channel; + switch (kind) { + case VK_BLEND_FACTOR_ZERO: return 0.0f; + case VK_BLEND_FACTOR_ONE: return 1.0f; + case VK_BLEND_FACTOR_SRC_COLOR: return source[index]; + case VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR: return 1.0f - source[index]; + case VK_BLEND_FACTOR_DST_COLOR: return destination[index]; + case VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR: return 1.0f - destination[index]; + case VK_BLEND_FACTOR_SRC_ALPHA: return source[3]; + case VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA: return 1.0f - source[3]; + case VK_BLEND_FACTOR_DST_ALPHA: return destination[3]; + case VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA: return 1.0f - destination[3]; + case VK_BLEND_FACTOR_CONSTANT_COLOR: return constants[index]; + case VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_COLOR: return 1.0f - constants[index]; + case VK_BLEND_FACTOR_CONSTANT_ALPHA: return constants[3]; + case VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_ALPHA: return 1.0f - constants[3]; + case VK_BLEND_FACTOR_SRC_ALPHA_SATURATE: return alpha ? 1.0f : std::fmin(source[3], 1.0f - destination[3]); + default: throw std::runtime_error("the oracle has no blend factor " + std::to_string(static_cast(kind))); + } +} + +float Equation(VkBlendOp operation, float source, float sourceFactor, float destination, float destinationFactor) { + volatile float weightedSource = source * sourceFactor; + volatile float weightedDestination = destination * destinationFactor; + switch (operation) { + case VK_BLEND_OP_ADD: return weightedSource + weightedDestination; + case VK_BLEND_OP_SUBTRACT: return weightedSource - weightedDestination; + case VK_BLEND_OP_REVERSE_SUBTRACT: return weightedDestination - weightedSource; + case VK_BLEND_OP_MIN: return std::fmin(source, destination); + case VK_BLEND_OP_MAX: return std::fmax(source, destination); + default: throw std::runtime_error("the oracle has no blend operation " + std::to_string(static_cast(operation))); + } +} + +float Decode(std::uint8_t byte, bool snorm) { + return snorm ? std::fmax(static_cast(static_cast(byte)) / 127.0f, -1.0f) : static_cast(byte) / 255.0f; +} + +std::uint8_t Encode(float value, bool snorm) { + if (std::isnan(value)) return 0; + if (snorm) return static_cast(static_cast(std::nearbyint(static_cast(std::fmin(std::fmax(value, -1.0f), 1.0f)) * 127.0))); + return static_cast(std::nearbyint(static_cast(std::fmin(std::fmax(value, 0.0f), 1.0f)) * 255.0)); +} + +std::array Blended(const BlendCase& blend, const std::array& rgba, const std::array& stored, bool clampInputs) { + std::array source = rgba; + std::array destination{}; + std::array constants = blend.constants; + const float low = blend.snorm ? -1.0f : 0.0f; + for (std::uint32_t channel = 0; channel < 4u; ++channel) destination[channel] = Decode(stored[channel], blend.snorm); + if (clampInputs) { + for (auto& value : source) value = std::fmin(std::fmax(value, low), 1.0f); + for (auto& value : constants) value = std::fmin(std::fmax(value, low), 1.0f); + } + std::array result = stored; + for (std::uint32_t channel = 0; channel < 4u; ++channel) { + if ((blend.writeMask & (1u << channel)) == 0u) continue; + const bool alpha = channel == 3u; + const auto sourceFactor = Factor(alpha ? blend.alphaSource : blend.colorSource, channel, alpha, source, destination, constants); + const auto destinationFactor = Factor(alpha ? blend.alphaDestination : blend.colorDestination, channel, alpha, source, destination, constants); + const float clampedFactorSource = clampInputs ? std::fmin(std::fmax(sourceFactor, 0.0f), 1.0f) : sourceFactor; + const float clampedFactorDestination = clampInputs ? std::fmin(std::fmax(destinationFactor, 0.0f), 1.0f) : destinationFactor; + result[channel] = Encode(Equation(alpha ? blend.alphaOperation : blend.colorOperation, source[channel], clampedFactorSource, destination[channel], clampedFactorDestination), blend.snorm); + } + return result; +} + +std::vector ReadBack(AgcDriver::VulkanDevice& device, const Block& block) { + AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(block.bytes); + AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +std::vector Fill(Block& block, std::uint32_t seed) { + std::vector bytes(block.bytes); + for (std::size_t index = 0; index < bytes.size(); ++index) bytes[index] = static_cast(((index + seed) * 0x9e3779b9u) >> 23u); + std::memcpy(block.data, bytes.data(), block.bytes); + return bytes; +} + +std::array Texel(const std::vector& bytes, const ColorTargetLayout& layout, std::uint32_t x, std::uint32_t y) { + const auto offset = layout.Offset(x, y); + return {bytes[offset], bytes[offset + 1u], bytes[offset + 2u], bytes[offset + 3u]}; +} + +struct Outcome { + bool distinguished = false; +}; + +int Difference(std::uint8_t left, std::uint8_t right, bool snorm) { + return snorm ? std::abs(static_cast(static_cast(left)) - static_cast(static_cast(right))) : std::abs(static_cast(left) - static_cast(right)); +} + +Outcome Expect(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, bool snorm, const std::string& what, const std::function(const std::array&)>& expected, const std::function(const std::array&)>& clamped) { + const auto stored = ReadBack(device, block); + Outcome outcome; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto was = Texel(before, layout, x, y); + const bool inside = x < ScissorWidth; + const auto want = inside ? expected(was) : was; + const auto got = Texel(stored, layout, x, y); + if (inside && clamped(was) != want) outcome.distinguished = true; + for (std::uint32_t channel = 0; channel < 4u; ++channel) { + if (Difference(got[channel], want[channel], snorm) <= (inside ? Tolerance : 0)) continue; + char detail[256]; + std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) %s the scissor holds (%u, %u, %u, %u), expected (%u, %u, %u, %u) from (%u, %u, %u, %u)", x, y, inside ? "inside" : "outside", got[0], got[1], got[2], got[3], want[0], want[1], want[2], want[3], was[0], was[1], was[2], was[3]); + throw std::runtime_error(what + detail); + } + } + } + return outcome; +} + +void RunCase(AgcDriver::VulkanDevice& device, Block& block, const ColorTargetLayout& layout, const AgcDriver::Graphics::ColorTarget& color, const Case& testCase, std::uint32_t seed) { + const auto before = Fill(block, seed); + Draw(device, color, testCase.blend, Float32Export, Float32Program(testCase.rgba)); + const auto outcome = Expect(device, block, layout, before, testCase.blend.snorm, std::string("unclamped blend ") + testCase.name, + [&](const std::array& was) { return Blended(testCase.blend, testCase.rgba, was, false); }, + [&](const std::array& was) { return Blended(testCase.blend, testCase.rgba, was, true); }); + Require(outcome.distinguished == testCase.distinguishesClamping, std::string("unclamped blend ") + testCase.name + (testCase.distinguishesClamping ? " does not differ from Vulkan's clamped blend, so it cannot catch a clamping approximation" : " differs from Vulkan's clamped blend, which the case declares equal")); +} + +void Overdraw(AgcDriver::VulkanDevice& device, Block& block, const ColorTargetLayout& layout, const AgcDriver::Graphics::ColorTarget& color, float first, float last, std::uint32_t seed) { + constexpr std::uint32_t Layers = 16; + static std::map>> stacks; + auto& vertices = stacks[last]; + std::vector depths; + for (std::uint32_t layer = 0; layer < Layers; ++layer) { + const float z = layer % 2u == Layers % 2u ? first : last; + depths.push_back(z); + const auto& triangle = TriangleAt(z); + vertices.insert(vertices.end(), triangle.begin(), triangle.end()); + } + BlendCase blend{VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_ADD, 0xf, {}, false}; + const auto before = Fill(block, seed); + Draw(device, color, blend, Float32Export, DepthAsXProgram({0.0f, 0.4f, 0.6f, 0.5f}), {DepthInput, 0u, vertices}); + Expect(device, block, layout, before, false, "one draw of 16 full-screen triangles alternating z " + std::to_string(first) + " and " + std::to_string(last) + " blended into a UNORM target", + [&](const std::array& was) { + auto state = was; + for (const auto z : depths) state = Blended(blend, {z, 0.4f, 0.6f, 0.5f}, state, false); + return state; + }, + [&](const std::array& was) { return was; }); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!TargetHasCapability(device->Target(), spv::CapabilityFragmentShaderPixelInterlockEXT)) { + std::puts("skipped, the device lacks fragmentShaderPixelInterlock"); + return VulkanTestSkipped; + } + const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 4); + Block block(layout.Bytes()); + const auto unorm = DecodeTarget(block, false); + const auto snorm = DecodeTarget(block, true); + constexpr VkColorComponentFlags All = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + constexpr auto Zero = VK_BLEND_FACTOR_ZERO; + constexpr auto One = VK_BLEND_FACTOR_ONE; + constexpr auto Add = VK_BLEND_OP_ADD; + const std::array constants{1.5f, -0.25f, 0.5f, 2.0f}; + const std::array unormCases{{ + {"SRC_ALPHA, ONE_MINUS_SRC_ALPHA (the Ghost of Yotei title scene setup) with an out-of-range source", {VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, Add, One, Zero, Add, All, {}, false}, {0.75f, -0.5f, 2.0f, 1.5f}, true}, + {"SRC_COLOR, ONE_MINUS_SRC_COLOR", {VK_BLEND_FACTOR_SRC_COLOR, VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR, Add, VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, Add, All, {}, false}, {1.5f, -0.25f, 0.5f, 0.75f}, true}, + {"DST_COLOR, ZERO", {VK_BLEND_FACTOR_DST_COLOR, Zero, Add, VK_BLEND_FACTOR_DST_ALPHA, Zero, Add, All, {}, false}, {2.0f, 0.5f, 3.0f, 1.25f}, true}, + {"ONE, ONE with a negative source", {One, One, Add, One, One, Add, All, {}, false}, {-0.5f, 0.25f, -2.0f, -0.75f}, true}, + {"SUBTRACT ONE, ONE with a large source", {One, One, VK_BLEND_OP_SUBTRACT, One, One, VK_BLEND_OP_SUBTRACT, All, {}, false}, {1.5f, 0.25f, 3.0f, 2.0f}, true}, + {"REVERSE_SUBTRACT SRC_ALPHA, ONE", {VK_BLEND_FACTOR_SRC_ALPHA, One, VK_BLEND_OP_REVERSE_SUBTRACT, One, One, VK_BLEND_OP_REVERSE_SUBTRACT, All, {}, false}, {0.5f, 2.0f, 0.25f, 2.0f}, true}, + {"MIN", {One, One, VK_BLEND_OP_MIN, One, One, VK_BLEND_OP_MIN, All, {}, false}, {-0.5f, 0.5f, 2.0f, 0.25f}, false}, + {"MAX", {Zero, Zero, VK_BLEND_OP_MAX, Zero, Zero, VK_BLEND_OP_MAX, All, {}, false}, {-0.5f, 0.5f, 2.0f, 0.25f}, false}, + {"CONSTANT_COLOR, ONE_MINUS_CONSTANT_COLOR with out-of-range constants", {VK_BLEND_FACTOR_CONSTANT_COLOR, VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_COLOR, Add, VK_BLEND_FACTOR_CONSTANT_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_ALPHA, Add, All, constants, false}, {0.5f, 0.75f, 0.25f, 0.5f}, true}, + {"SRC_ALPHA_SATURATE, ONE", {VK_BLEND_FACTOR_SRC_ALPHA_SATURATE, One, Add, One, One, Add, All, {}, false}, {1.5f, 2.0f, 0.5f, 3.0f}, true}, + {"DST_ALPHA, ONE_MINUS_DST_ALPHA", {VK_BLEND_FACTOR_DST_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA, Add, One, Zero, Add, All, {}, false}, {2.0f, -1.0f, 0.5f, 0.5f}, true}, + {"separate alpha equation: color ADD SRC_ALPHA, alpha MAX", {VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, Add, One, One, VK_BLEND_OP_MAX, All, {}, false}, {1.25f, 0.5f, 0.75f, 1.75f}, true}, + {"write mask R and B", {VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, Add, One, Zero, Add, VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_B_BIT, {}, false}, {0.75f, 2.0f, 1.5f, 1.5f}, true}, + {"write mask none", {One, One, Add, One, One, Add, 0, {}, false}, {0.75f, 2.0f, 1.5f, 1.5f}, false}, + }}; + const std::array snormCases{{ + {"SNORM SRC_ALPHA, ONE_MINUS_SRC_ALPHA", {VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, Add, One, Zero, Add, All, {}, true}, {-0.75f, 1.5f, -2.0f, 1.5f}, true}, + {"SNORM DST_COLOR, ZERO", {VK_BLEND_FACTOR_DST_COLOR, Zero, Add, VK_BLEND_FACTOR_DST_ALPHA, Zero, Add, All, {}, true}, {-2.0f, 0.5f, 3.0f, -1.25f}, true}, + {"SNORM ONE, ONE", {One, One, Add, One, One, Add, All, {}, true}, {-1.5f, 0.25f, -0.5f, 2.0f}, true}, + {"SNORM SUBTRACT ONE, ONE", {One, One, VK_BLEND_OP_SUBTRACT, One, One, VK_BLEND_OP_SUBTRACT, All, {}, true}, {1.5f, -2.0f, 0.25f, 0.5f}, true}, + }}; + std::uint32_t seed = 1; + for (const auto& testCase : unormCases) RunCase(*device, block, layout, unorm, testCase, seed++); + for (const auto& testCase : snormCases) RunCase(*device, block, layout, snorm, testCase, seed++); + Overdraw(*device, block, layout, unorm, 0.2f, 0.8f, 401u); + Overdraw(*device, block, layout, unorm, 0.8f, 0.2f, 402u); + std::printf("unclamped normalized color blend tests passed: %zu UNORM draws, %zu SNORM draws, 2 same-draw overdraws of 16 layers\n", unormCases.size(), snormCases.size()); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 915e32a2ebc763c11c13432c55cb280342f16a80 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 26 Oct 2026 15:24:33 +1300 Subject: [PATCH 015/160] docs(agc): record the unclamped normalized color blend path --- docs/dev/debt/libSceAgcDriver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 8f66ab33c1..cb27629e62 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -258,7 +258,7 @@ - [Resource cache entries holding released textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled texture the texture cache drops while something else holds it, or makes without caching (`admitTexture` refusing a first use), is marked released, and `ResourceCache::Insert` refuses descriptor sets that hold one and removes the entries that do. The removal waits for the next insert: until then `Find` still returns those entries and their textures stay alive. One process-wide flag starts the sweep, so while the sampled cache is over its budget every insert scans all entries (1,024 at start, up to 16,384 after growth, or `APS5_RESOURCE_CACHE_ENTRIES`, each with its textures); that cost is not measured. When `ReleaseTextureCaches` runs on device memory exhaustion, the sampled textures these entries hold are only marked released and are freed at the next insert, so the immediate allocation retry gets no memory back from them. A draw or dispatch whose textures the cache refused builds its descriptor sets again on each use for as long as the cache stays full; the count bounds (`APS5_RESOURCE_CACHE_ENTRIES`, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) still bound nothing by bytes, and sampled textures made outside the texture cache (the disabled cache, depth-surface copies) are never marked. Measured on one title on one RTX 4090: with the default bounds, sampled textures held outside the cache peaked at 2,836 MiB (7,830 MiB before) and device-local heap 0 stayed under its 22.9 GB budget for the whole run (26,357 MiB before, which aborted with `VK_ERROR_OUT_OF_DEVICE_MEMORY` at the wolf scene). Whether the extra descriptor-set builds cost frame time elsewhere is not measured. - [Sampled MSAA surface geometry](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) supplies the fragment-aware linear size and one mip for a tiled 2D surface at two, four or eight fragments. Unsupported arrays, mip chains, extents and block sizes throw. Linear descriptors retain the existing geometry path, including its use when sampling an existing native target; a linear MSAA guest layout is not modeled. The range calculation follows AMD Addrlib's thin-block dimensions. Console layout equivalence and title effects of the additional mip metadata remain unmeasured. This does not add sample upload or readback. -- [Unclamped normalized color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - CB_COLOR_INFO targets with BLEND_CLAMP clear (bit 15) on UNORM and SNORM formats are accepted when blending is disabled (ENABLE clear or BLEND_BYPASS set). Vulkan converts unblended shader values and clamps to the normalized range automatically; blending into an unclamped normalized target remains unmodeled because Vulkan always clamps inputs prior to blending. +- [Unclamped normalized color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - CB_COLOR_INFO targets with BLEND_CLAMP clear (bit 15) on UNORM and SNORM formats are accepted. Without blending Vulkan's conversion clamps the stored value. With blending the pixel shader reads the target through its storage image under pixel interlock, evaluates the blend equation on the unclamped export, and stores the clamped result, because Vulkan clamps blend inputs to the normalized range. The result is not measured on a console. The final float to normalized rounding is Vulkan's. The path needs fragmentShaderPixelInterlock, recorded draws and a single-sample resident target, and refuses dual-source blending, multisampled, packed, reversed and alternate component order targets, and a second merged target in one draw. - [Multisample image readback](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleReadback.cpp) fetches unsigned R8/R16/R32/R32G32/R32G32B32A32 or D32 depth samples through a 2D array view in GENERAL, in layer/y/x/fragment order. The caller owns producer completion, image transitions and resource lifetimes. It implements Vulkan per-sample fetches, not guest-memory publication, metadata expansion or PS5 layout equivalence; D32 exceptional-value preservation has not been measured. Other formats, sample counts and invalid ranges throw. - [Multisample image upload](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleUpload.cpp) does not model guest layouts, native ownership or CMASK/FMASK/DCC publication; console equivalence and large-title initialization costs remain unmeasured. It stores 1/2/4/8/16-byte elements from a storage buffer into an unsigned 2D multisample array view in layer/y/x/fragment order. It requires enabled multisample storage images and formatless image writes, 2/4/8 fragments and valid aligned buffer and dispatch ranges. The caller supplies the matching storage view in GENERAL and owns barriers, completion and resource lifetimes. From 0ba98673a6ef3bbeaf4663084d016e9c0e18b697 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 26 Oct 2026 16:42:28 +1300 Subject: [PATCH 016/160] fix(agc): refuse a signed unclamped blend that skips a component and name every merged target The shader rewrites a component the write mask skips through a float, which turns the signed normalized code -128 into -127. A signed target whose write mask skips a component is refused. The one-merged-target refusals no longer say packed, and blending unclamped normalized color checks for another merged target. --- .../libSceAgcDriver/Graphics/src/State.cpp | 24 ++++++++++++++----- docs/dev/debt/libSceAgcDriver.md | 2 +- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 5ef6146d39..b6bdc6d7fb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1408,6 +1408,20 @@ void ApplyDepthRemap(const Context& context, State& state) { state.depthClamp = true; } +static bool SignedNormalizedFormat(VkFormat format) { + return format == VK_FORMAT_R8_SNORM || format == VK_FORMAT_R16_SNORM || format == VK_FORMAT_R8G8_SNORM || format == VK_FORMAT_R16G16_SNORM || format == VK_FORMAT_R8G8B8A8_SNORM || format == VK_FORMAT_B8G8R8A8_SNORM || format == VK_FORMAT_R16G16B16A16_SNORM; +} + +static std::uint32_t NormalizedChannelMask(VkFormat format) { + switch (format) { + case VK_FORMAT_R8_SNORM: + case VK_FORMAT_R16_SNORM: return 0x1u; + case VK_FORMAT_R8G8_SNORM: + case VK_FORMAT_R16G16_SNORM: return 0x3u; + default: return 0xfu; + } +} + State DecodeState(const QueueState& queue, bool vertexWorkgroup) { if (auto reason = DepthMaintenanceRejection(queue); !reason.empty()) throw std::runtime_error(reason); const auto& cx = queue.context; @@ -1758,7 +1772,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { Require(nibbles || (exportedMask & packedMask) == packedMask, "partial writes of " + packed + " color target are unsupported"); if ((exportedMask & packedMask) != packedMask) { Require(color.mipCount == 1u && color.depth == 1u && color.layers == 1u && color.arrayLastSlice == 0u, "partial writes of a mipmapped, volume or array " + std::string(PackedColorTargetName(color.packing)) + " color target are not implemented"); - Require(std::ranges::none_of(result.colors, [](const ColorTarget& other) { return other.partialWriteMask != 0; }), "partial writes of more than one packed color target are not implemented"); + Require(std::ranges::none_of(result.colors, [](const ColorTarget& other) { return other.partialWriteMask != 0; }), "a draw merges into one color target at most, so partial writes of a packed color target cannot join another merged color target"); color.partialWriteMask = PackedWriteMask(mapping, exportedMask); state.colorWriteMask = 0; if (color.samples > 1u) { @@ -1789,6 +1803,8 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { Require(color.samples == 1u && color.packing == ShaderRecompiler::ColorExportPacking::None, "blending unclamped normalized color into a multisampled or packed color target is not implemented" + where); Require(!reversed && mapping != 0xecu, "blending unclamped normalized color into a color target with a reversed or alternate component order is not implemented" + where); Require(!(secondSource(state.srcColorBlendFactor) || secondSource(state.dstColorBlendFactor) || secondSource(state.srcAlphaBlendFactor) || secondSource(state.dstAlphaBlendFactor)), "dual-source blending of unclamped normalized color is not implemented" + where); + Require(std::ranges::none_of(result.colors, [&](const ColorTarget& other) { return &other != &color && other.partialWriteMask != 0; }), "a draw merges into one color target at most, so blending unclamped normalized color cannot join another merged color target" + where); + Require(!SignedNormalizedFormat(color.format) || (state.colorWriteMask & NormalizedChannelMask(color.format)) == NormalizedChannelMask(color.format), "blending unclamped signed normalized color with a write mask that skips a component is not implemented, since the shader rewrites the skipped component through a float and the code -128 would become -127" + where); color.partialWriteMask = UnclampedBlendMask; } result.blends[color.exportIndex] = state; @@ -1858,16 +1874,12 @@ bool DepthStencilTests(const State& state) { return state.depth.has_value() && (state.depthTest || state.stencilTest || state.depthBoundsTest); } -static bool SignedNormalizedFormat(VkFormat format) { - return format == VK_FORMAT_R8_SNORM || format == VK_FORMAT_R16_SNORM || format == VK_FORMAT_R8G8_SNORM || format == VK_FORMAT_R16G16_SNORM || format == VK_FORMAT_R8G8B8A8_SNORM || format == VK_FORMAT_B8G8R8A8_SNORM || format == VK_FORMAT_R16G16B16A16_SNORM; -} - ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state) { static_assert(VK_BLEND_FACTOR_ZERO == 0 && VK_BLEND_FACTOR_SRC_ALPHA_SATURATE == 14 && VK_BLEND_OP_ADD == 0 && VK_BLEND_OP_MAX == 4); ShaderRecompiler::TargetMerge merge; for (const auto& color : state.colors) { if (color.partialWriteMask == 0) continue; - Require(merge.mask == 0 && color.exportIndex < 8u && color.samples <= 8u, "a draw merges into one packed color target at most"); + Require(merge.mask == 0 && color.exportIndex < 8u && color.samples <= 8u, "a draw merges into one color target at most"); Require(color.samples == 1u || state.perSampleShading, "a merged multisampled color target needs per-sample shading"); merge.target = static_cast(color.exportIndex); merge.mask = color.partialWriteMask; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index cb27629e62..f5d507a381 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -258,7 +258,7 @@ - [Resource cache entries holding released textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled texture the texture cache drops while something else holds it, or makes without caching (`admitTexture` refusing a first use), is marked released, and `ResourceCache::Insert` refuses descriptor sets that hold one and removes the entries that do. The removal waits for the next insert: until then `Find` still returns those entries and their textures stay alive. One process-wide flag starts the sweep, so while the sampled cache is over its budget every insert scans all entries (1,024 at start, up to 16,384 after growth, or `APS5_RESOURCE_CACHE_ENTRIES`, each with its textures); that cost is not measured. When `ReleaseTextureCaches` runs on device memory exhaustion, the sampled textures these entries hold are only marked released and are freed at the next insert, so the immediate allocation retry gets no memory back from them. A draw or dispatch whose textures the cache refused builds its descriptor sets again on each use for as long as the cache stays full; the count bounds (`APS5_RESOURCE_CACHE_ENTRIES`, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) still bound nothing by bytes, and sampled textures made outside the texture cache (the disabled cache, depth-surface copies) are never marked. Measured on one title on one RTX 4090: with the default bounds, sampled textures held outside the cache peaked at 2,836 MiB (7,830 MiB before) and device-local heap 0 stayed under its 22.9 GB budget for the whole run (26,357 MiB before, which aborted with `VK_ERROR_OUT_OF_DEVICE_MEMORY` at the wolf scene). Whether the extra descriptor-set builds cost frame time elsewhere is not measured. - [Sampled MSAA surface geometry](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) supplies the fragment-aware linear size and one mip for a tiled 2D surface at two, four or eight fragments. Unsupported arrays, mip chains, extents and block sizes throw. Linear descriptors retain the existing geometry path, including its use when sampling an existing native target; a linear MSAA guest layout is not modeled. The range calculation follows AMD Addrlib's thin-block dimensions. Console layout equivalence and title effects of the additional mip metadata remain unmeasured. This does not add sample upload or readback. -- [Unclamped normalized color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - CB_COLOR_INFO targets with BLEND_CLAMP clear (bit 15) on UNORM and SNORM formats are accepted. Without blending Vulkan's conversion clamps the stored value. With blending the pixel shader reads the target through its storage image under pixel interlock, evaluates the blend equation on the unclamped export, and stores the clamped result, because Vulkan clamps blend inputs to the normalized range. The result is not measured on a console. The final float to normalized rounding is Vulkan's. The path needs fragmentShaderPixelInterlock, recorded draws and a single-sample resident target, and refuses dual-source blending, multisampled, packed, reversed and alternate component order targets, and a second merged target in one draw. +- [Unclamped normalized color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - CB_COLOR_INFO targets with BLEND_CLAMP clear (bit 15) on UNORM and SNORM formats are accepted. Without blending Vulkan's conversion clamps the stored value. With blending the pixel shader reads the target through its storage image under pixel interlock, evaluates the blend equation on the unclamped export, and stores the clamped result, because Vulkan clamps blend inputs to the normalized range. The result is not measured on a console. The final float to normalized rounding is Vulkan's. The path needs fragmentShaderPixelInterlock, recorded draws and a single-sample resident target, and refuses dual-source blending, multisampled, packed, reversed and alternate component order targets, a signed target whose write mask skips a component (the shader rewrites it through a float, so the code -128 would become -127), and a second merged target in one draw. - [Multisample image readback](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleReadback.cpp) fetches unsigned R8/R16/R32/R32G32/R32G32B32A32 or D32 depth samples through a 2D array view in GENERAL, in layer/y/x/fragment order. The caller owns producer completion, image transitions and resource lifetimes. It implements Vulkan per-sample fetches, not guest-memory publication, metadata expansion or PS5 layout equivalence; D32 exceptional-value preservation has not been measured. Other formats, sample counts and invalid ranges throw. - [Multisample image upload](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleUpload.cpp) does not model guest layouts, native ownership or CMASK/FMASK/DCC publication; console equivalence and large-title initialization costs remain unmeasured. It stores 1/2/4/8/16-byte elements from a storage buffer into an unsigned 2D multisample array view in layer/y/x/fragment order. It requires enabled multisample storage images and formatless image writes, 2/4/8 fragments and valid aligned buffer and dispatch ranges. The caller supplies the matching storage view in GENERAL and owns barriers, completion and resource lifetimes. From b2f850f9c156ea9b93fad1788ad2496b8fbd9b48 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 26 Oct 2026 16:42:28 +1300 Subject: [PATCH 017/160] test(agc): cover the blend request bytes, key, masks and the missing pixel interlock --- .../prx/libSceAgcDriver/tests/Graphics.cpp | 8 +++++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 33 +++++++++++++++++++ .../execution/UnclampedNormalizedBlend.cpp | 15 +++++++-- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 4e3d552e99..2e8eeeadff 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1794,6 +1794,14 @@ void UnclampedNormalizedColorTests() { queue.context[0x1e0] = blending; state = DecodeState(queue); Require(state.colors.at(0).partialWriteMask == AgcDriver::Graphics::UnclampedBlendMask && AgcDriver::Graphics::ExportTargetMerge(state).blend.snorm, "an unclamped SNORM target that blends was not routed to the pixel shader as signed"); + auto signedMasked = queue; + signedMasked.context[0x8e] = 0x1u; + expectFailure([&] { DecodeState(signedMasked); }, "blending unclamped signed normalized color with a write mask that skips a component is not implemented"); + auto unsignedMasked = target(0x0000000cu); + unsignedMasked.context[0x1e0] = blending; + unsignedMasked.context[0x8e] = 0x1u; + state = DecodeState(unsignedMasked); + Require(state.colors.at(0).partialWriteMask == AgcDriver::Graphics::UnclampedBlendMask && state.blends.at(0).colorWriteMask == VK_COLOR_COMPONENT_R_BIT, "an unclamped UNORM target that blends with a write mask that skips a component was not routed to the pixel shader"); queue.context[0x1e0] = (1u << 30u) | 15u | (15u << 8u); expectFailure([&] { DecodeState(queue); }, "dual-source blending of unclamped normalized color is not implemented (CB_COLOR_INFO 0x0000010c, CB_BLEND0_CONTROL 0x40000f0f)"); queue = target(0x00000604u); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 2a31594300..b8129eb282 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -5606,6 +5606,30 @@ void verifyPixelRequestSerialization() { depthTestingMerge.context.pixel->targetMerge.depthStencilTests = true; RecompileCacheKey::Build(depthTestingMerge, replayKey); require(key != replayKey, "the merge draw's depth or stencil tests did not change the static shader identity"); + auto blending = request; + blending.context.pixel->targetMerge.blend = {true, false, 6u, 7u, 0u, 1u, 0u, 0u, 0xfu, {}}; + RecompileCacheKey::Build(blending, replayKey); + require(key != replayKey && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(blending), "an unclamped color target blend did not change the static shader identity"); + const auto blendKey = replayKey; + for (const auto change : {0, 1, 2, 3, 4, 5, 6, 7, 8}) { + auto other = blending; + auto& blend = other.context.pixel->targetMerge.blend; + if (change == 0) blend.snorm = true; + if (change == 1) blend.colorSource = 2u; + if (change == 2) blend.colorDestination = 2u; + if (change == 3) blend.colorOperation = 1u; + if (change == 4) blend.alphaSource = 2u; + if (change == 5) blend.alphaDestination = 2u; + if (change == 6) blend.alphaOperation = 1u; + if (change == 7) blend.writeMask = 0x5u; + if (change == 8) blend.constants[2] = 0x3f800000u; + RecompileCacheKey::Build(other, replayKey); + require(blendKey != replayKey, "a field of the unclamped color target blend did not change the static shader identity"); + } + auto disabled = blending; + disabled.context.pixel->targetMerge.blend.enabled = false; + RecompileCacheKey::Build(disabled, replayKey); + require(key == replayKey && RecompileCacheKey::ContextHash(request) == RecompileCacheKey::ContextHash(disabled), "a disabled color target blend changed the static shader identity"); auto withoutMerge = request; withoutMerge.context.pixel->targetMerge = {}; RecompileCacheKey::Build(withoutMerge, replayKey); @@ -5905,6 +5929,15 @@ void verifyVertexInfoVersion19() { const auto mergingBytes = decodeBase64(serializer.Serialize(merging)); require(mergingBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + mergeOffset, mergingBytes.begin()) && mergingBytes[mergeOffset] == 3u && mergingBytes[mergeOffset + 1] == 0x0fu && mergingBytes[mergeOffset + 2] == 0xf0u && mergingBytes[mergeOffset + 5] == 2u && mergingBytes[mergeOffset + 6] == 1u && mergingBytes[mergeOffset + 34] == 0xefu && mergingBytes[mergeOffset + 37] == 0x89u && mergingBytes[mergeOffset + 38] == 1u && mergingBytes[mergeOffset + 39] == 1u && std::equal(plainPixel.begin() + mergeOffset + static_cast(mergeBytes), plainPixel.end(), mergingBytes.begin() + mergeOffset + static_cast(mergeBytes)), "the color target merge is not the 40 bytes the version 46 pixel info adds after the sample order"); require(serializer.Deserialize(serializer.Serialize(merging)).request.context.pixel->targetMerge == merging.context.pixel->targetMerge, "a serialized request lost its color target merge"); + auto blendMerging = pixel; + blendMerging.context.pixel->targetMerge = {1u, 0xffffu, 1u, {1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u}, true, false, {true, true, 6u, 7u, 1u, 1u, 0u, 4u, 0xbu, {0x3fc00000u, 0xbe800000u, 0x3f000000u, 0x40000000u}}}; + const auto blendMergingBytes = decodeBase64(serializer.Serialize(blendMerging)); + constexpr std::size_t blendPayloadBytes = 24u; + require(blendMergingBytes.size() == plainPixel.size() + blendPayloadBytes && std::equal(plainPixel.begin(), plainPixel.begin() + mergeOffset, blendMergingBytes.begin()) && blendMergingBytes[mergeOffset] == 1u && blendMergingBytes[mergeOffset + 1] == 0xffu && blendMergingBytes[mergeOffset + 2] == 0xffu && blendMergingBytes[mergeOffset + 3] == 0x01u && blendMergingBytes[mergeOffset + 4] == 0x00u, "a color target blend is not flagged in the merge mask word, or changed the bytes before the merge"); + require(serializer.Deserialize(serializer.Serialize(blendMerging)).request.context.pixel->targetMerge == blendMerging.context.pixel->targetMerge, "a serialized request lost its color target blend"); + auto disabledBlend = merging; + disabledBlend.context.pixel->targetMerge.blend = {false, true, 6u, 7u, 1u, 1u, 0u, 4u, 0xbu, {1u, 2u, 3u, 4u}}; + require(serializer.Serialize(disabledBlend) == serializer.Serialize(merging), "a disabled color target blend changed the serialized request"); // A version 43 request: no color target merge, and no fused arithmetic flag, pixel function LDS flag or storage size at the end. const auto version43Bytes = [&](std::vector bytes) { require(bytes.end() - bytes.begin() > mergeOffset + static_cast(mergeBytes) && std::equal(plainPixel.begin() + mergeOffset, plainPixel.begin() + mergeOffset + static_cast(mergeBytes), bytes.begin() + mergeOffset), "a replay fixture does not hold the default color target merge after the sample order"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/UnclampedNormalizedBlend.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/UnclampedNormalizedBlend.cpp index 3b4498ca6e..7a217937d1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/UnclampedNormalizedBlend.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/UnclampedNormalizedBlend.cpp @@ -106,6 +106,7 @@ struct DrawSetup { std::span> vertices = Triangle; std::uint64_t depth = 0; bool depthWrite = false; + bool withoutInterlock = false; }; struct Block { @@ -180,7 +181,10 @@ struct BlendCase { void Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTarget color, const BlendCase& blendCase, std::uint32_t exportFormat, std::span pixelCode, const DrawSetup& setup = {}) { - const auto target = device.Target(); + auto target = device.Target(); + std::vector capabilities(target.supportedCapabilities.begin(), target.supportedCapabilities.end()); + if (setup.withoutInterlock) std::erase(capabilities, static_cast(spv::CapabilityFragmentShaderPixelInterlockEXT)); + target.supportedCapabilities = capabilities; constexpr std::uint32_t waveSize = 64; std::vector vertexUserData(4, 0u); const auto address = reinterpret_cast(setup.vertices.data()); @@ -475,9 +479,16 @@ int main() { std::uint32_t seed = 1; for (const auto& testCase : unormCases) RunCase(*device, block, layout, unorm, testCase, seed++); for (const auto& testCase : snormCases) RunCase(*device, block, layout, snorm, testCase, seed++); + std::string refusal; + try { + Draw(*device, unorm, unormCases[0].blend, Float32Export, Float32Program(unormCases[0].rgba), {.withoutInterlock = true}); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find("blending into an unclamped normalized color target needs the fragmentShaderPixelInterlock feature, which the device lacks") != std::string::npos, "a device without fragmentShaderPixelInterlock did not refuse the unclamped blend: " + refusal); Overdraw(*device, block, layout, unorm, 0.2f, 0.8f, 401u); Overdraw(*device, block, layout, unorm, 0.8f, 0.2f, 402u); - std::printf("unclamped normalized color blend tests passed: %zu UNORM draws, %zu SNORM draws, 2 same-draw overdraws of 16 layers\n", unormCases.size(), snormCases.size()); + std::printf("unclamped normalized color blend tests passed: %zu UNORM draws, %zu SNORM draws, 2 same-draw overdraws of 16 layers, 1 refusal without pixel interlock\n", unormCases.size(), snormCases.size()); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From 37935cdec1e09f098b67208f253bfd18fff24c64 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 26 Oct 2026 19:22:15 +1300 Subject: [PATCH 018/160] test(agc): require channels outside the write mask to be exact The blended-pixel check allowed one step of difference on every channel inside the scissor. Channels the write mask skips now use the same zero tolerance as pixels outside the scissor, so a stray write to a masked channel fails the test. --- .../tests/execution/UnclampedNormalizedBlend.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/UnclampedNormalizedBlend.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/UnclampedNormalizedBlend.cpp index 7a217937d1..a95b32f66f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/UnclampedNormalizedBlend.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/UnclampedNormalizedBlend.cpp @@ -382,7 +382,7 @@ int Difference(std::uint8_t left, std::uint8_t right, bool snorm) { return snorm ? std::abs(static_cast(static_cast(left)) - static_cast(static_cast(right))) : std::abs(static_cast(left) - static_cast(right)); } -Outcome Expect(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, bool snorm, const std::string& what, const std::function(const std::array&)>& expected, const std::function(const std::array&)>& clamped) { +Outcome Expect(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, bool snorm, std::uint32_t writeMask, const std::string& what, const std::function(const std::array&)>& expected, const std::function(const std::array&)>& clamped) { const auto stored = ReadBack(device, block); Outcome outcome; for (std::uint32_t y = 0; y < Height; ++y) { @@ -393,7 +393,8 @@ Outcome Expect(AgcDriver::VulkanDevice& device, const Block& block, const ColorT const auto got = Texel(stored, layout, x, y); if (inside && clamped(was) != want) outcome.distinguished = true; for (std::uint32_t channel = 0; channel < 4u; ++channel) { - if (Difference(got[channel], want[channel], snorm) <= (inside ? Tolerance : 0)) continue; + const bool written = inside && (writeMask & (1u << channel)) != 0u; + if (Difference(got[channel], want[channel], snorm) <= (written ? Tolerance : 0)) continue; char detail[256]; std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) %s the scissor holds (%u, %u, %u, %u), expected (%u, %u, %u, %u) from (%u, %u, %u, %u)", x, y, inside ? "inside" : "outside", got[0], got[1], got[2], got[3], want[0], want[1], want[2], want[3], was[0], was[1], was[2], was[3]); throw std::runtime_error(what + detail); @@ -406,7 +407,7 @@ Outcome Expect(AgcDriver::VulkanDevice& device, const Block& block, const ColorT void RunCase(AgcDriver::VulkanDevice& device, Block& block, const ColorTargetLayout& layout, const AgcDriver::Graphics::ColorTarget& color, const Case& testCase, std::uint32_t seed) { const auto before = Fill(block, seed); Draw(device, color, testCase.blend, Float32Export, Float32Program(testCase.rgba)); - const auto outcome = Expect(device, block, layout, before, testCase.blend.snorm, std::string("unclamped blend ") + testCase.name, + const auto outcome = Expect(device, block, layout, before, testCase.blend.snorm, testCase.blend.writeMask, std::string("unclamped blend ") + testCase.name, [&](const std::array& was) { return Blended(testCase.blend, testCase.rgba, was, false); }, [&](const std::array& was) { return Blended(testCase.blend, testCase.rgba, was, true); }); Require(outcome.distinguished == testCase.distinguishesClamping, std::string("unclamped blend ") + testCase.name + (testCase.distinguishesClamping ? " does not differ from Vulkan's clamped blend, so it cannot catch a clamping approximation" : " differs from Vulkan's clamped blend, which the case declares equal")); @@ -426,7 +427,7 @@ void Overdraw(AgcDriver::VulkanDevice& device, Block& block, const ColorTargetLa BlendCase blend{VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_ADD, 0xf, {}, false}; const auto before = Fill(block, seed); Draw(device, color, blend, Float32Export, DepthAsXProgram({0.0f, 0.4f, 0.6f, 0.5f}), {DepthInput, 0u, vertices}); - Expect(device, block, layout, before, false, "one draw of 16 full-screen triangles alternating z " + std::to_string(first) + " and " + std::to_string(last) + " blended into a UNORM target", + Expect(device, block, layout, before, false, blend.writeMask, "one draw of 16 full-screen triangles alternating z " + std::to_string(first) + " and " + std::to_string(last) + " blended into a UNORM target", [&](const std::array& was) { auto state = was; for (const auto z : depths) state = Blended(blend, {z, 0.4f, 0.6f, 0.5f}, state, false); From 3134ed1658c4da281d3b77346fa804e0c6a67a94 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Tue, 27 Oct 2026 02:35:01 +0000 Subject: [PATCH 019/160] test(libkernel): cover sceKernelAllocateMainDirectMemory's block and argument checks Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestMemory.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index df4e2041c9..e53539e0be 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -537,6 +537,24 @@ static void CheckDirectMemoryFollowsPhysicalPages() { Require(sceKernelReleaseDirectMemory(again, page * 2) == 0); } +static void CheckAllocateMainDirectMemory() { + constexpr std::size_t page = 0x4000; + std::int64_t physical = -1; + Require(sceKernelAllocateMainDirectMemory(page * 2, page * 2, 3, &physical) == 0); + Require(physical >= 0 && physical % static_cast(page * 2) == 0 && static_cast(physical) + page * 2 <= sceKernelGetDirectMemorySize()); + int type = -1; + std::int64_t start = -1; + std::int64_t end = -1; + Require(sceKernelGetDirectMemoryType(physical, &type, &start, &end) == 0 && type == 3 && start == physical && end == physical + static_cast(page * 2)); + Require(sceKernelReleaseDirectMemory(physical, page * 2) == 0); + std::int64_t untouched = -1; + Require(sceKernelAllocateMainDirectMemory(0, 0, 0, &untouched) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelAllocateMainDirectMemory(0x1000, 0, 0, &untouched) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelAllocateMainDirectMemory(page, 0x1000, 0, &untouched) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelAllocateMainDirectMemory(page, 0, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(untouched == -1); +} + static void CheckSparseDirectMemoryBacking() { constexpr std::size_t page = 0x4000; constexpr std::size_t size = 256u * 1024u * 1024u; @@ -3341,6 +3359,7 @@ int main(int argc, char** argv) { CheckPosixMprotectIgnoresUndefinedBits(); CheckDirectMemoryFollowsPhysicalPages(); CheckSparseDirectMemoryBacking(); + CheckAllocateMainDirectMemory(); CheckSavedDirectMemorySurvivesCachePressure(); CheckReusedDirectMemorySections(); CheckReleaseDirectMemoryClearsMappings(); From 3b779dee14ecedce88c967110bc238a07f47d927 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Tue, 27 Oct 2026 02:50:04 +0000 Subject: [PATCH 020/160] test(libkernel): move the main direct memory test clear of the macOS direct memory change Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestMemory.cpp | 38 ++++++++++++++++----------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index e53539e0be..9216190e6d 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -537,24 +537,6 @@ static void CheckDirectMemoryFollowsPhysicalPages() { Require(sceKernelReleaseDirectMemory(again, page * 2) == 0); } -static void CheckAllocateMainDirectMemory() { - constexpr std::size_t page = 0x4000; - std::int64_t physical = -1; - Require(sceKernelAllocateMainDirectMemory(page * 2, page * 2, 3, &physical) == 0); - Require(physical >= 0 && physical % static_cast(page * 2) == 0 && static_cast(physical) + page * 2 <= sceKernelGetDirectMemorySize()); - int type = -1; - std::int64_t start = -1; - std::int64_t end = -1; - Require(sceKernelGetDirectMemoryType(physical, &type, &start, &end) == 0 && type == 3 && start == physical && end == physical + static_cast(page * 2)); - Require(sceKernelReleaseDirectMemory(physical, page * 2) == 0); - std::int64_t untouched = -1; - Require(sceKernelAllocateMainDirectMemory(0, 0, 0, &untouched) == SCE_KERNEL_ERROR_EINVAL); - Require(sceKernelAllocateMainDirectMemory(0x1000, 0, 0, &untouched) == SCE_KERNEL_ERROR_EINVAL); - Require(sceKernelAllocateMainDirectMemory(page, 0x1000, 0, &untouched) == SCE_KERNEL_ERROR_EINVAL); - Require(sceKernelAllocateMainDirectMemory(page, 0, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); - Require(untouched == -1); -} - static void CheckSparseDirectMemoryBacking() { constexpr std::size_t page = 0x4000; constexpr std::size_t size = 256u * 1024u * 1024u; @@ -627,6 +609,24 @@ static void CheckSavedDirectMemorySurvivesCachePressure() { #endif } +static void CheckAllocateMainDirectMemory() { + constexpr std::size_t page = 0x4000; + std::int64_t physical = -1; + Require(sceKernelAllocateMainDirectMemory(page * 2, page * 2, 3, &physical) == 0); + Require(physical >= 0 && physical % static_cast(page * 2) == 0 && static_cast(physical) + page * 2 <= sceKernelGetDirectMemorySize()); + int type = -1; + std::int64_t start = -1; + std::int64_t end = -1; + Require(sceKernelGetDirectMemoryType(physical, &type, &start, &end) == 0 && type == 3 && start == physical && end == physical + static_cast(page * 2)); + Require(sceKernelReleaseDirectMemory(physical, page * 2) == 0); + std::int64_t untouched = -1; + Require(sceKernelAllocateMainDirectMemory(0, 0, 0, &untouched) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelAllocateMainDirectMemory(0x1000, 0, 0, &untouched) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelAllocateMainDirectMemory(page, 0x1000, 0, &untouched) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelAllocateMainDirectMemory(page, 0, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(untouched == -1); +} + static void CheckReusedDirectMemorySections() { #ifdef _WIN32 constexpr std::size_t page = 0x4000; @@ -3359,9 +3359,9 @@ int main(int argc, char** argv) { CheckPosixMprotectIgnoresUndefinedBits(); CheckDirectMemoryFollowsPhysicalPages(); CheckSparseDirectMemoryBacking(); - CheckAllocateMainDirectMemory(); CheckSavedDirectMemorySurvivesCachePressure(); CheckReusedDirectMemorySections(); + CheckAllocateMainDirectMemory(); CheckReleaseDirectMemoryClearsMappings(); CheckReleaseDirectMemoryRejectsInvalidRanges(); CheckFixedMappingReplacesPartialOverlap(); From 8de35f251c68ebd7bd9ed8cff4810021024eea11 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Tue, 27 Oct 2026 15:52:00 +1300 Subject: [PATCH 021/160] test(agc): drop the assertions that pin unsupported blend forms to throw The dual-source blend and the signed blend that skips a component are valid forms the decoder still refuses; the tests no longer pin their refusal, which the production guards in State.cpp keep. --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 2e8eeeadff..aeb74c0775 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1794,16 +1794,11 @@ void UnclampedNormalizedColorTests() { queue.context[0x1e0] = blending; state = DecodeState(queue); Require(state.colors.at(0).partialWriteMask == AgcDriver::Graphics::UnclampedBlendMask && AgcDriver::Graphics::ExportTargetMerge(state).blend.snorm, "an unclamped SNORM target that blends was not routed to the pixel shader as signed"); - auto signedMasked = queue; - signedMasked.context[0x8e] = 0x1u; - expectFailure([&] { DecodeState(signedMasked); }, "blending unclamped signed normalized color with a write mask that skips a component is not implemented"); auto unsignedMasked = target(0x0000000cu); unsignedMasked.context[0x1e0] = blending; unsignedMasked.context[0x8e] = 0x1u; state = DecodeState(unsignedMasked); Require(state.colors.at(0).partialWriteMask == AgcDriver::Graphics::UnclampedBlendMask && state.blends.at(0).colorWriteMask == VK_COLOR_COMPONENT_R_BIT, "an unclamped UNORM target that blends with a write mask that skips a component was not routed to the pixel shader"); - queue.context[0x1e0] = (1u << 30u) | 15u | (15u << 8u); - expectFailure([&] { DecodeState(queue); }, "dual-source blending of unclamped normalized color is not implemented (CB_COLOR_INFO 0x0000010c, CB_BLEND0_CONTROL 0x40000f0f)"); queue = target(0x00000604u); expectFailure([&] { DecodeState(queue); }, "unclamped normalized color is unsupported"); queue = target(0x0000800cu); From 3d1463aef6ba25b976f024f4004713f31844cf80 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Tue, 27 Oct 2026 03:04:33 +0000 Subject: [PATCH 022/160] test(libkernel): keep only the main direct memory allocation and type check Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestMemory.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 9216190e6d..17cdead5ad 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -619,12 +619,6 @@ static void CheckAllocateMainDirectMemory() { std::int64_t end = -1; Require(sceKernelGetDirectMemoryType(physical, &type, &start, &end) == 0 && type == 3 && start == physical && end == physical + static_cast(page * 2)); Require(sceKernelReleaseDirectMemory(physical, page * 2) == 0); - std::int64_t untouched = -1; - Require(sceKernelAllocateMainDirectMemory(0, 0, 0, &untouched) == SCE_KERNEL_ERROR_EINVAL); - Require(sceKernelAllocateMainDirectMemory(0x1000, 0, 0, &untouched) == SCE_KERNEL_ERROR_EINVAL); - Require(sceKernelAllocateMainDirectMemory(page, 0x1000, 0, &untouched) == SCE_KERNEL_ERROR_EINVAL); - Require(sceKernelAllocateMainDirectMemory(page, 0, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); - Require(untouched == -1); } static void CheckReusedDirectMemorySections() { From 70540892790ad25003c59d56d8eb6df7497ad91b Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 21:44:12 +0000 Subject: [PATCH 023/160] test(libc): name the failing check in guest_math --- core/libs/tests/GuestMath.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index a305ccc335..0d0670f446 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include static_assert(sizeof(GuestFloatingPointEnvironment) == 32); @@ -51,7 +52,11 @@ extern DinkumwareFloatCharacteristics _Flt_nid_postfix; extern DinkumwareFloatCharacteristics _Dbl_nid_postfix; extern DinkumwareFloatCharacteristics _Ldbl_nid_postfix; } -static void Require(bool value) { if (!value) std::abort(); } +static void Require(bool value, std::source_location location = std::source_location::current()) { + if (value) return; + std::fprintf(stderr, "%s:%u: requirement failed\n", location.file_name(), location.line()); + std::abort(); +} constexpr int StandardExceptions = FE_INVALID | FE_DIVBYZERO | FE_OVERFLOW | FE_UNDERFLOW | FE_INEXACT; static void CheckSquareRoot() { From bc46a0ade909c8a54e1373db977ce06985fefe5b Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 21:44:12 +0000 Subject: [PATCH 024/160] test(libc): name the failing check in the exact math test --- core/libs/tests/GuestMathExact.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestMathExact.cpp b/core/libs/tests/GuestMathExact.cpp index 5db64f610d..f43f49f24b 100644 --- a/core/libs/tests/GuestMathExact.cpp +++ b/core/libs/tests/GuestMathExact.cpp @@ -5,10 +5,16 @@ #include #include #include +#include #include #include +#include -static void Require(bool value) { if (!value) std::abort(); } +static void Require(bool value, std::source_location location = std::source_location::current()) { + if (value) return; + std::fprintf(stderr, "%s:%u: requirement failed\n", location.file_name(), location.line()); + std::abort(); +} [[gnu::noinline]] static long double Extended(std::uint16_t signExponent, std::uint64_t mantissa) { long double value = 0; From 99652eb0048946b491a695c3dba29db3df0ff493 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Sun, 25 Oct 2026 21:44:12 +0000 Subject: [PATCH 025/160] test(libc): name the failing check in the float builtins test --- core/libs/tests/GuestFloatBuiltins.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestFloatBuiltins.cpp b/core/libs/tests/GuestFloatBuiltins.cpp index e8051b96c0..5595a78ecc 100644 --- a/core/libs/tests/GuestFloatBuiltins.cpp +++ b/core/libs/tests/GuestFloatBuiltins.cpp @@ -3,11 +3,17 @@ #include #include #include +#include #include #include +#include #include -static void Require(bool value) { if (!value) std::abort(); } +static void Require(bool value, std::source_location location = std::source_location::current()) { + if (value) return; + std::fprintf(stderr, "%s:%u: requirement failed\n", location.file_name(), location.line()); + std::abort(); +} static std::uint64_t Bits(double value) { return std::bit_cast(value); } static std::uint32_t Bits(float value) { return std::bit_cast(value); } From 9cd6de6c4f14f3ee57a24947852cbb6a2b88658b Mon Sep 17 00:00:00 2001 From: MarcZX10 Date: Thu, 22 Oct 2026 04:58:50 +0000 Subject: [PATCH 026/160] fix(agc): bound registered shader preparation memory --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 +++ .../Shaders/RegistrationPreparationBudget.hpp | 14 ++++++ .../Execution/include/PerformanceOverlay.hpp | 1 + .../src/Driver/Shaders/ShaderRegistry.cpp | 45 ++++++++++++++++++- .../Execution/src/PerformanceOverlay.cpp | 18 ++++++++ .../tests/RegistrationPrepareBudget.cpp | 28 ++++++++++++ 6 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/RegistrationPreparationBudget.hpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/RegistrationPrepareBudget.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 0924d1dd2a..ef0b211a4f 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2210,6 +2210,10 @@ set_target_properties(agc_driver_mesh_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_mesh_tests) set(ANYPS5_AGC_EXECUTION_TESTS "" CACHE STRING "tests/execution sources to build, by file name without extension; the others build a stub that reports a skip. Empty builds all") +add_executable(agc_driver_registration_prepare_budget_tests tests/RegistrationPrepareBudget.cpp) +target_include_directories(agc_driver_registration_prepare_budget_tests PRIVATE ${LIBS_INCLUDE_DIR}) +set_target_properties(agc_driver_registration_prepare_budget_tests PROPERTIES CXX_EXTENSIONS OFF) + file(GLOB agcExecutionTestSources CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/tests/execution/*.cpp) set(agcExecutionTests) foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources) @@ -2277,6 +2281,8 @@ foreach(agcExecutionTestName IN LISTS ANYPS5_AGC_EXECUTION_TESTS) endif() endforeach() if(BUILD_TESTING) + add_test(NAME agc_driver_registration_prepare_budget COMMAND agc_driver_registration_prepare_budget_tests) + set_tests_properties(agc_driver_registration_prepare_budget PROPERTIES TIMEOUT 10) add_test(NAME agc_driver_indirect_thread_dimensions_cpu COMMAND agc_driver_indirect_thread_dimensions_tests) set_tests_properties(agc_driver_indirect_thread_dimensions_cpu PROPERTIES ENVIRONMENT "APS5_NO_GPU_THREAD_DIMENSIONS=1" SKIP_RETURN_CODE 77 TIMEOUT 60) if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/RegistrationPreparationBudget.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/RegistrationPreparationBudget.hpp new file mode 100644 index 0000000000..a24f8d8f60 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/RegistrationPreparationBudget.hpp @@ -0,0 +1,14 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_SHADERS_REGISTRATIONPREPARATIONBUDGET_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_SHADERS_REGISTRATIONPREPARATIONBUDGET_HPP + +#include + +namespace AgcDriver::DriverDetail { + +constexpr bool RegistrationPreparationAllowed(std::uint64_t processPrivateBytes, std::uint64_t commitBudgetBytes) noexcept { + return processPrivateBytes < commitBudgetBytes; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp index 8eca6e6190..9bb2f2aaa8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp @@ -257,6 +257,7 @@ std::optional ReadProcessPrivateBytes(); std::optional ParseCommitCharge(std::string_view meminfo); std::optional ReadCommitCharge(); std::optional ReadPageFaults(); +std::optional ReadPhysicalMemoryBytes(); std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content); OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, OverlayCorner corner); std::array, OverlayColorCount> OverlayPalette(VkFormat format, VkColorSpaceKHR colorSpace); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 3a69eaa097..33d1019b1f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -6,8 +6,10 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/RegistrationPreparationBudget.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include "CacheKey.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp" @@ -18,6 +20,8 @@ #include #include #include +#include +#include #include #include #include @@ -33,6 +37,7 @@ #include #include #include +#include #include #include #ifdef _WIN32 @@ -1705,14 +1710,45 @@ bool AsyncRegistrationPrepare() { return !sync; } +std::uint64_t RegistrationPrepareCommitBudgetBytes() { + static const std::uint64_t budget = [] { + constexpr double GiB = 1024.0 * 1024.0 * 1024.0; + if (const auto* text = std::getenv("APS5_REGISTRATION_PREPARE_COMMIT_GB")) { + char* end = nullptr; + errno = 0; + const auto value = std::strtod(text, &end); + if (errno != 0 || end == text || *end != '\0' || !std::isfinite(value) || value < 0 || value >= static_cast(std::numeric_limits::max()) / GiB) { + throw std::runtime_error("APS5_REGISTRATION_PREPARE_COMMIT_GB must be a non-negative number of GiB"); + } + return static_cast(value * GiB); + } + if (const auto physical = ReadPhysicalMemoryBytes()) return *physical / 2; + return std::numeric_limits::max(); + }(); + return budget; +} + +bool RegistrationPrepareWithinMemoryBudget() { + const auto privateBytes = ReadProcessPrivateBytes(); + return !privateBytes || RegistrationPreparationAllowed(*privateBytes, RegistrationPrepareCommitBudgetBytes()); +} + +void LogRegistrationPrepareDeferred() { + static std::atomic reported{false}; + if (reported.exchange(true, std::memory_order_relaxed)) return; + APS5_LOG_OUT("%s", "process private memory reached the registered shader preparation budget; later registrations are prepared when first used (APS5_REGISTRATION_PREPARE_COMMIT_GB)"); +} + void PrepareRegisteredInBackground(std::shared_ptr snapshot, std::shared_ptr plan) { const auto* preparedKey = snapshot->prepared.get(); SubmitRegistrationPreparation([snapshot = std::move(snapshot), plan = std::move(plan)] { PerformanceContext timingContext(FrameTiming::Preparation()); std::vector entries; std::exception_ptr failure; + bool deferred = false; try { - entries = PrepareRegistered(*plan); + if (RegistrationPrepareWithinMemoryBudget()) entries = PrepareRegistered(*plan); + else deferred = true; } catch (const std::exception& error) { APS5_LOG_ERR("Shader 0x%llx snapshot=0x%llx could not be prepared after registration: %s", static_cast(snapshot->codeAddress), static_cast(reinterpret_cast(snapshot.get())), error.what()); failure = std::current_exception(); @@ -1723,10 +1759,12 @@ void PrepareRegisteredInBackground(std::shared_ptr snapsho { std::lock_guard lock(prepared.mutex); prepared.entries.insert(prepared.entries.end(), std::make_move_iterator(entries.begin()), std::make_move_iterator(entries.end())); - if (failure == nullptr && !plan->abiKey.empty()) prepared.registeredAbis.push_back(std::move(plan->abiKey)); + if (failure == nullptr && !deferred && !plan->abiKey.empty()) prepared.registeredAbis.push_back(std::move(plan->abiKey)); + prepared.deferred = prepared.deferred || deferred; prepared.failure = failure; prepared.pending = false; } + if (deferred) LogRegistrationPrepareDeferred(); prepared.settled.notify_all(); }, preparedKey); } @@ -2391,6 +2429,9 @@ void Driver::RegisterShader(const Shader* shader) { if (LazyShaderPreparation()) { static_cast(PlanRegistered(snapshot, *localDevice, registered, true, &snapshot.prepared->deferred)); snapshot.prepared->deferred = true; + } else if (!RegistrationPrepareWithinMemoryBudget()) { + snapshot.prepared->deferred = true; + LogRegistrationPrepareDeferred(); } else if (AsyncRegistrationPrepare()) { preparation = PlanRegistered(snapshot, *localDevice, registered, true, &snapshot.prepared->deferred); if (preparation != nullptr) preparation->prebuildDevice = localDevice; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp index e0c805d9d4..ae384a38d8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp @@ -15,6 +15,7 @@ #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include #include +#include #include #include #include @@ -25,6 +26,9 @@ #include #include #include +#ifndef _WIN32 +#include +#endif #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #ifndef NOMINMAX @@ -678,6 +682,20 @@ std::optional ReadPageFaults() { #endif } +std::optional ReadPhysicalMemoryBytes() { +#ifdef _WIN32 + MEMORYSTATUSEX status{}; + status.dwLength = sizeof(status); + if (::GlobalMemoryStatusEx(&status) == 0) return std::nullopt; + return static_cast(status.ullTotalPhys); +#else + const auto pages = ::sysconf(_SC_PHYS_PAGES); + const auto pageSize = ::sysconf(_SC_PAGESIZE); + if (pages <= 0 || pageSize <= 0) return std::nullopt; + return static_cast(pages) * static_cast(pageSize); +#endif +} + std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content) { auto scale = std::max(1, static_cast((extent.height + 240) / 480)); while (scale > 1 && !fits(extent, content, scale)) --scale; diff --git a/core/libs/prx/libSceAgcDriver/tests/RegistrationPrepareBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/RegistrationPrepareBudget.cpp new file mode 100644 index 0000000000..55a0f9684f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/RegistrationPrepareBudget.cpp @@ -0,0 +1,28 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/RegistrationPreparationBudget.hpp" +#include +#include +#include + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +} + +int main() { + try { + using AgcDriver::DriverDetail::RegistrationPreparationAllowed; + Require(RegistrationPreparationAllowed(0, 1), "preparation should be allowed below the budget"); + Require(RegistrationPreparationAllowed(1023, 1024), "preparation should be allowed immediately below the budget"); + Require(!RegistrationPreparationAllowed(1024, 1024), "preparation should defer at the budget"); + Require(!RegistrationPreparationAllowed(1025, 1024), "preparation should defer above the budget"); + Require(!RegistrationPreparationAllowed(0, 0), "a zero budget should defer every preparation"); + std::cout << "registration prepare budget tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From c7be816c49b18a443bc946a6b90f16b5eee1ca19 Mon Sep 17 00:00:00 2001 From: MarcZX10 Date: Thu, 22 Oct 2026 04:58:50 +0000 Subject: [PATCH 027/160] docs(agc): document shader preparation memory budget --- docs/dev/ENVIRONMENT.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 5f175220c4..f497b33f2f 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -374,6 +374,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_PROFILE_GPU` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records the GPU time of recorded work by key from batch timestamps | | `APS5_PROFILE_GPU_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp`, `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | with APS5_PROFILE_GPU, records the GPU time of each recorded draw by its fragment program's variant id and prints a [drawvariant] line with the code address behind each variant id once (draws that do not use a recipe only; the [gputime] line labels each `draw 0x`, the id [drawvariant] prints, and the ranges share the batch's query cap) | | `APS5_REAP_EACH_PACKET` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | reaps deferred labels after every packet | +| `APS5_REGISTRATION_PREPARE_COMMIT_GB` | number | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp` | half of physical RAM | stops preparing newly registered shaders ahead of use when process-private memory reaches this GiB budget; `0` disables registration-time preparation | | `APS5_RELEASE_ON_UNLOCK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | destroys deferred batches on the unlocking thread instead of a release thread | | `APS5_RELEASE_QUEUE_MAX` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | 256 | maximum deferred batches handed to the release thread before a thread destroys its own | | `APS5_RELEASE_UNDER_LOCK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | destroys deferred batches in finish() under the lock instead of on a release thread | From 964a4fee1273570929be55d7c3e87c9897bfb65a Mon Sep 17 00:00:00 2001 From: MarcZX10 Date: Thu, 22 Oct 2026 05:00:02 +0000 Subject: [PATCH 028/160] test(agc): cover maximum registration budget --- .../prx/libSceAgcDriver/tests/RegistrationPrepareBudget.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/RegistrationPrepareBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/RegistrationPrepareBudget.cpp index 55a0f9684f..e586e46bd2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/RegistrationPrepareBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/RegistrationPrepareBudget.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/RegistrationPreparationBudget.hpp" #include #include +#include #include namespace { @@ -19,6 +20,9 @@ int main() { Require(!RegistrationPreparationAllowed(1024, 1024), "preparation should defer at the budget"); Require(!RegistrationPreparationAllowed(1025, 1024), "preparation should defer above the budget"); Require(!RegistrationPreparationAllowed(0, 0), "a zero budget should defer every preparation"); + const auto unlimited = std::numeric_limits::max(); + Require(RegistrationPreparationAllowed(unlimited - 1, unlimited), "the maximum budget should allow values below it"); + Require(!RegistrationPreparationAllowed(unlimited, unlimited), "the maximum budget should defer at its limit"); std::cout << "registration prepare budget tests passed\n"; return 0; } catch (const std::exception& error) { From 9235d74b8d0826e216248bce07dc39c321ca9c42 Mon Sep 17 00:00:00 2001 From: MarcZX10 Date: Thu, 22 Oct 2026 05:09:26 +0000 Subject: [PATCH 029/160] refactor(agc): gate registration in shader planner --- .../src/Driver/Shaders/ShaderRegistry.cpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 33d1019b1f..1be9e8dfe7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -6,10 +6,8 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" -#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/RegistrationPreparationBudget.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" -#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include "CacheKey.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp" @@ -55,6 +53,8 @@ #include #include #include "Optimization/ShaderStageInputInfo.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/RegistrationPreparationBudget.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include #include "RdnaDecoder/RdnaInstructionDecoder.hpp" @@ -1214,7 +1214,15 @@ struct RegisteredPreparation { std::shared_ptr prebuildDevice; }; +bool RegistrationPrepareWithinMemoryBudget(); +void LogRegistrationPrepareDeferred(); + std::unique_ptr PlanRegistered(const ShaderSnapshot& snapshot, const VulkanDevice& device, const QueueState& state, bool registration, bool* deferred = nullptr) { + if (registration && std::getenv("APS5_LAZY_SHADER_PREPARE") == nullptr && !RegistrationPrepareWithinMemoryBudget()) { + if (deferred != nullptr) *deferred = true; + LogRegistrationPrepareDeferred(); + return {}; + } using Stage = ShaderRecompiler::ShaderStage; const auto header = ReadHeader(snapshot); std::uint32_t programRegister; @@ -2429,9 +2437,6 @@ void Driver::RegisterShader(const Shader* shader) { if (LazyShaderPreparation()) { static_cast(PlanRegistered(snapshot, *localDevice, registered, true, &snapshot.prepared->deferred)); snapshot.prepared->deferred = true; - } else if (!RegistrationPrepareWithinMemoryBudget()) { - snapshot.prepared->deferred = true; - LogRegistrationPrepareDeferred(); } else if (AsyncRegistrationPrepare()) { preparation = PlanRegistered(snapshot, *localDevice, registered, true, &snapshot.prepared->deferred); if (preparation != nullptr) preparation->prebuildDevice = localDevice; From b4b2a48c7e2054b452049a9b8c3ddecf876a63db Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:39:28 +0000 Subject: [PATCH 030/160] test(agc): validate registration preparation budget parsing --- .../Shaders/RegistrationPreparationBudget.hpp | 17 +++++++++++++++++ .../src/Driver/Shaders/ShaderRegistry.cpp | 15 +++------------ .../tests/RegistrationPrepareBudget.cpp | 16 ++++++++++++++++ 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/RegistrationPreparationBudget.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/RegistrationPreparationBudget.hpp index a24f8d8f60..716339e5f7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/RegistrationPreparationBudget.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/RegistrationPreparationBudget.hpp @@ -1,10 +1,27 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_SHADERS_REGISTRATIONPREPARATIONBUDGET_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_SHADERS_REGISTRATIONPREPARATIONBUDGET_HPP +#include +#include +#include #include +#include +#include namespace AgcDriver::DriverDetail { +inline std::uint64_t RegistrationPreparationCommitBudgetBytes(const char* text, std::uint64_t defaultBudgetBytes) { + if (text == nullptr) return defaultBudgetBytes; + constexpr double GiB = 1024.0 * 1024.0 * 1024.0; + char* end = nullptr; + errno = 0; + const auto value = std::strtod(text, &end); + if (errno != 0 || end == text || *end != '\0' || !std::isfinite(value) || value < 0 || value >= static_cast(std::numeric_limits::max()) / GiB) { + throw std::runtime_error("APS5_REGISTRATION_PREPARE_COMMIT_GB must be a non-negative number of GiB"); + } + return static_cast(value * GiB); +} + constexpr bool RegistrationPreparationAllowed(std::uint64_t processPrivateBytes, std::uint64_t commitBudgetBytes) noexcept { return processPrivateBytes < commitBudgetBytes; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 1be9e8dfe7..f600b8674e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1720,18 +1720,9 @@ bool AsyncRegistrationPrepare() { std::uint64_t RegistrationPrepareCommitBudgetBytes() { static const std::uint64_t budget = [] { - constexpr double GiB = 1024.0 * 1024.0 * 1024.0; - if (const auto* text = std::getenv("APS5_REGISTRATION_PREPARE_COMMIT_GB")) { - char* end = nullptr; - errno = 0; - const auto value = std::strtod(text, &end); - if (errno != 0 || end == text || *end != '\0' || !std::isfinite(value) || value < 0 || value >= static_cast(std::numeric_limits::max()) / GiB) { - throw std::runtime_error("APS5_REGISTRATION_PREPARE_COMMIT_GB must be a non-negative number of GiB"); - } - return static_cast(value * GiB); - } - if (const auto physical = ReadPhysicalMemoryBytes()) return *physical / 2; - return std::numeric_limits::max(); + const auto physicalBytes = ReadPhysicalMemoryBytes(); + const auto defaultBudget = physicalBytes ? *physicalBytes / 2 : std::numeric_limits::max(); + return RegistrationPreparationCommitBudgetBytes(std::getenv("APS5_REGISTRATION_PREPARE_COMMIT_GB"), defaultBudget); }(); return budget; } diff --git a/core/libs/prx/libSceAgcDriver/tests/RegistrationPrepareBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/RegistrationPrepareBudget.cpp index e586e46bd2..5fc52facac 100644 --- a/core/libs/prx/libSceAgcDriver/tests/RegistrationPrepareBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/RegistrationPrepareBudget.cpp @@ -14,7 +14,23 @@ void Require(bool condition, const char* message) { int main() { try { + using AgcDriver::DriverDetail::RegistrationPreparationCommitBudgetBytes; using AgcDriver::DriverDetail::RegistrationPreparationAllowed; + constexpr std::uint64_t GiB = 1024ull * 1024 * 1024; + constexpr std::uint64_t defaultBudget = 8ull * GiB; + Require(RegistrationPreparationCommitBudgetBytes(nullptr, defaultBudget) == defaultBudget, "an unset budget should preserve the host-derived default"); + Require(RegistrationPreparationCommitBudgetBytes("0", defaultBudget) == 0, "zero GiB should disable registration preparation"); + Require(RegistrationPreparationCommitBudgetBytes("0.5", defaultBudget) == GiB / 2, "fractional GiB should convert to bytes"); + Require(RegistrationPreparationCommitBudgetBytes("2", defaultBudget) == GiB * 2, "whole GiB should convert to bytes"); + for (const auto* invalid : {"", "-1", "1x", "inf", "nan", "1e1000"}) { + bool rejected = false; + try { + static_cast(RegistrationPreparationCommitBudgetBytes(invalid, defaultBudget)); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected, "invalid budget text should be rejected"); + } Require(RegistrationPreparationAllowed(0, 1), "preparation should be allowed below the budget"); Require(RegistrationPreparationAllowed(1023, 1024), "preparation should be allowed immediately below the budget"); Require(!RegistrationPreparationAllowed(1024, 1024), "preparation should defer at the budget"); From a1389c142e27730c8ba0702725c9cee5cb60aa3d Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:43:11 +0000 Subject: [PATCH 031/160] fix(agc): honor registration budget override early --- .../Execution/src/Driver/Shaders/ShaderRegistry.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index f600b8674e..5d24318b96 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1720,9 +1720,12 @@ bool AsyncRegistrationPrepare() { std::uint64_t RegistrationPrepareCommitBudgetBytes() { static const std::uint64_t budget = [] { + if (const auto* text = std::getenv("APS5_REGISTRATION_PREPARE_COMMIT_GB")) { + return RegistrationPreparationCommitBudgetBytes(text, 0); + } const auto physicalBytes = ReadPhysicalMemoryBytes(); const auto defaultBudget = physicalBytes ? *physicalBytes / 2 : std::numeric_limits::max(); - return RegistrationPreparationCommitBudgetBytes(std::getenv("APS5_REGISTRATION_PREPARE_COMMIT_GB"), defaultBudget); + return RegistrationPreparationCommitBudgetBytes(nullptr, defaultBudget); }(); return budget; } From d2579939b93d577522d90c6d70ff3feb04ea3d43 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 01:24:20 +0000 Subject: [PATCH 032/160] build(agc): link the registration budget test statically on MinGW with unwind tables On a DWARF-exception MinGW toolchain core/libs builds with -fno-asynchronous-unwind-tables and this host-only test links no guest libc, so __gxx_personality_v0 was undefined. It uses the settings patched_libraries_tests uses. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index ef0b211a4f..9634f894f6 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2213,6 +2213,10 @@ set(ANYPS5_AGC_EXECUTION_TESTS "" CACHE STRING "tests/execution sources to build add_executable(agc_driver_registration_prepare_budget_tests tests/RegistrationPrepareBudget.cpp) target_include_directories(agc_driver_registration_prepare_budget_tests PRIVATE ${LIBS_INCLUDE_DIR}) set_target_properties(agc_driver_registration_prepare_budget_tests PROPERTIES CXX_EXTENSIONS OFF) +if(MINGW) + target_compile_options(agc_driver_registration_prepare_budget_tests PRIVATE -fasynchronous-unwind-tables) + target_link_options(agc_driver_registration_prepare_budget_tests PRIVATE -static -static-libgcc -static-libstdc++) +endif() file(GLOB agcExecutionTestSources CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/tests/execution/*.cpp) set(agcExecutionTests) From 7301d0241c1421d0fb37e5773c679629980e06ff Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Tue, 27 Oct 2026 01:26:41 +0000 Subject: [PATCH 033/160] test(agc): run the lazy shader preparation test with a registration budget of 0 With APS5_REGISTRATION_PREPARE_COMMIT_GB=0 a registration is left to its first use, without APS5_LAZY_SHADER_PREPARE. The test pins that the registered shader has no prepared entries and is marked deferred, that its first dispatch prepares it and that the output is right. Registration-time validation failures move to first use then, so the three registration failure checks only run in the other lazy mode. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 +++-- .../tests/execution/PreparedShaders.cpp | 24 +++++++++++-------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 9634f894f6..00e866da87 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -3283,7 +3283,7 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_array_layer_target_cpu PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - foreach(preparedMode IN ITEMS indirect uncached debug debug_failure deferred deferred_undecodable registered_workgroup registered_workgroup_sync dispatch_ahead_prewarm abi_deferred_compute abi_deferred_pixel abi_deferred_stages abi_deferred_rectangle abi_deferred_undecodable_state abi_deferred_registration_failure abi_deferred_vertex_inputs abi_deferred_rectangle_pairs lazy lazy_sync) + foreach(preparedMode IN ITEMS indirect uncached debug debug_failure deferred deferred_undecodable registered_workgroup registered_workgroup_sync dispatch_ahead_prewarm abi_deferred_compute abi_deferred_pixel abi_deferred_stages abi_deferred_rectangle abi_deferred_undecodable_state abi_deferred_registration_failure abi_deferred_vertex_inputs abi_deferred_rectangle_pairs lazy lazy_sync lazy_budget_zero) set(preparedTest "agc_driver_prepared_shaders_${preparedMode}") if(preparedMode MATCHES "^abi_deferred_") string(REPLACE "_" "-" preparedArgument ${preparedMode}) @@ -3294,7 +3294,7 @@ if(BUILD_TESTING) add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --deferred) elseif(preparedMode STREQUAL "deferred_undecodable") add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --deferred-undecodable) - elseif(preparedMode STREQUAL "lazy" OR preparedMode STREQUAL "lazy_sync") + elseif(preparedMode STREQUAL "lazy" OR preparedMode STREQUAL "lazy_sync" OR preparedMode STREQUAL "lazy_budget_zero") add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --lazy) elseif(preparedMode STREQUAL "registered_workgroup" OR preparedMode STREQUAL "registered_workgroup_sync") add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --registered-workgroup) @@ -3318,6 +3318,8 @@ if(BUILD_TESTING) if(preparedMode STREQUAL "lazy_sync") set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_SYNC_SHADER_PREPARE=1") endif() + elseif(preparedMode STREQUAL "lazy_budget_zero") + set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_REGISTRATION_PREPARE_COMMIT_GB=0" "APS5_SUSPEND_DRAIN=1") elseif(preparedMode STREQUAL "debug" OR preparedMode STREQUAL "debug_failure") set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_PROFILE_DRAW=1" "APS5_NO_UNLOCKED_DEVICE=1") endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index 94b7c946eb..5d199de3c5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -651,7 +651,9 @@ void RegisteredVertexWorkgroup(const ShaderRecompiler::SpirvTarget& target, bool void LazyRegistration() { using namespace NarrowConstantStoreFixture; - Require(std::getenv("APS5_LAZY_SHADER_PREPARE") != nullptr, "lazy preparation is not enabled"); + const char* registrationBudget = std::getenv("APS5_REGISTRATION_PREPARE_COMMIT_GB"); + const bool budgetOnly = std::getenv("APS5_LAZY_SHADER_PREPARE") == nullptr; + Require(!budgetOnly || (registrationBudget != nullptr && std::string_view(registrationBudget) == "0"), "lazy preparation is not enabled"); struct Header { Shader shader{}; std::array registers{}; @@ -669,15 +671,17 @@ void LazyRegistration() { header.specials.dispatch_modifier = 0x8000; header.registers = {{{0x20c, static_cast(address >> 8u)}, {0x20d, static_cast(address >> 40u)}, {0x207, Threads}, {0x208, 1}, {0x209, 1}, {0x212, 0}, {0x213, 4}}}; AgcDriverRegisterShader_nid_postfix(&header.shader); - ++header.registers[0].value; - ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "entry point is outside shader code"); - --header.registers[0].value; - header.registers[2].value = 0; - ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "must be nonzero"); - header.registers[2].value = Threads; - header.shader.num_sh_registers = 255; - ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "truncated shader metadata"); - header.shader.num_sh_registers = header.registers.size(); + if (!budgetOnly) { + ++header.registers[0].value; + ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "entry point is outside shader code"); + --header.registers[0].value; + header.registers[2].value = 0; + ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "must be nonzero"); + header.registers[2].value = Threads; + header.shader.num_sh_registers = 255; + ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "truncated shader metadata"); + header.shader.num_sh_registers = header.registers.size(); + } const auto registered = [&] { const auto registry = AgcDriver::DriverDetail::Driver::Get().RegisteredShaders(); Require(registry != nullptr, "lazy registration published no shader registry"); From 365e00176a838ff7c11eb3fa501b1420c036bf04 Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Tue, 27 Oct 2026 04:47:33 -0300 Subject: [PATCH 034/160] test(libc): check case-ambiguous names only on a case-sensitive host directory --- core/libs/tests/GuestPathCase.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/core/libs/tests/GuestPathCase.cpp b/core/libs/tests/GuestPathCase.cpp index 675bd28809..2e3ba29a12 100644 --- a/core/libs/tests/GuestPathCase.cpp +++ b/core/libs/tests/GuestPathCase.cpp @@ -60,13 +60,15 @@ int main() { #ifndef _WIN32 std::filesystem::create_directory_symlink("Data", directory / "Linked"); CheckRead("linked/SETTINGS.INI", 'x'); - { std::ofstream file(directory / "Data" / "SETTINGS.INI"); file << 'z'; } - CheckRead("Data/Settings.ini", 'x'); - CheckRead("Data/SETTINGS.INI", 'z'); - bool ambiguous = false; - try { CheckRead("data/settings.ini", 'x'); } catch (const std::runtime_error&) { ambiguous = true; } - Require(ambiguous); - std::filesystem::remove(directory / "Data" / "SETTINGS.INI"); + if (!std::filesystem::exists(directory / "Data" / "SETTINGS.INI")) { + { std::ofstream file(directory / "Data" / "SETTINGS.INI"); file << 'z'; } + CheckRead("Data/Settings.ini", 'x'); + CheckRead("Data/SETTINGS.INI", 'z'); + bool ambiguous = false; + try { CheckRead("data/settings.ini", 'x'); } catch (const std::runtime_error&) { ambiguous = true; } + Require(ambiguous); + std::filesystem::remove(directory / "Data" / "SETTINGS.INI"); + } std::filesystem::remove(directory / "Data" / "Settings.ini"); { std::ofstream file(directory / "Data" / "SETTINGS.ini"); file << 'n'; } CheckRead("data/settings.ini", 'n'); From fcd1898618991f56b78c702204602c7d3482f97f Mon Sep 17 00:00:00 2001 From: milescoviello Date: Tue, 27 Oct 2026 08:42:24 +0000 Subject: [PATCH 035/160] docs(libSceNpUniversalDataSystem): list the context registration and handle teardown as silent stubs Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libSceNpUniversalDataSystem.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/dev/debt/libSceNpUniversalDataSystem.md b/docs/dev/debt/libSceNpUniversalDataSystem.md index d9602f375d..dd9a81c121 100644 --- a/docs/dev/debt/libSceNpUniversalDataSystem.md +++ b/docs/dev/debt/libSceNpUniversalDataSystem.md @@ -1,5 +1,9 @@ # libSceNpUniversalDataSystem technical debt +### Silent stubs + +- [sceNpUniversalDataSystemRegisterContext, sceNpUniversalDataSystemDestroyHandle and sceNpUniversalDataSystemAbortHandle](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp) (libSceNpUniversalDataSystem) - return `SCE_NP_UNIVERSAL_DATA_SYSTEM_OK` without checking the context or handle and without recording anything. `sceNpUniversalDataSystemPostEvent` uses only its context and ignores the handle, so nothing depends on the registration. All ten titles in my set call the first two; Metal Gear Solid 4 (PPSA18194) also calls `sceNpUniversalDataSystemAbortHandle`. + ### Unknown function info - [Events](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): `sceNpUniversalDataSystemCreateEvent` copies `prop` and returns the event's own property object through `prop_ptr`, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/libTrophy.cpp) does; `sceNpUniversalDataSystemEventPropertyObjectSetObject`, `SetArray` and the array setters for objects and arrays store a copy of `value` and return the stored copy through `value_ptr`. Destroying a property object or array that its Create function did not return (an event's property object or a stored copy) does nothing, as its owner frees it. These ownership rules are not verified on a console, nor are the output of `sceNpUniversalDataSystemEventToString` (still `{}`) and `sceNpUniversalDataSystemEventEstimateSize` (still 3). From 1b3db98f483dee42ba4880ec661f903b1774c628 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Tue, 27 Oct 2026 08:53:24 +0000 Subject: [PATCH 036/160] docs(libSceNpTrophy2): list the context registration and handle teardown as silent stubs Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libSceNpTrophy2.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceNpTrophy2.md b/docs/dev/debt/libSceNpTrophy2.md index 8552c9e0de..223a7c3de1 100644 --- a/docs/dev/debt/libSceNpTrophy2.md +++ b/docs/dev/debt/libSceNpTrophy2.md @@ -4,6 +4,7 @@ - [sceNpTrophy2GetRewardIcon](../../../core/libs/prx/libSceNpTrophy2/src/Context.cpp) (libSceNpTrophy2) - zeroes size and returns `SCE_NP_TROPHY2_ERROR_ICON_FILE_NOT_FOUND` (-2141898479): the `(int context, int handle, int reward_id, void* buffer, size_t* size)` signature and two-stage query pattern follow the bundled `Media/Plugins/PSNCore.prx` of PPSA06888 and SharpProspero; returning -2141898479 (0x80553911) when icons are unavailable is inferred from sibling calls (sceNpTrophy2GetGameIcon, sceNpTrophy2GetGroupIcon, sceNpTrophy2GetTrophyIcon) in KytyPS5 (libTrophy.cpp:464/483/502) - [libSceNpTrophy2](../../../core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp): `sceNpTrophy2RegisterUnlockCallback` accepts a callback that is never called, as the callback's signature and when the console calls it are not known, `sceNpTrophy2UnregisterUnlockCallback` only returns success, and `sceNpTrophy2ShowTrophyList` returns success without showing a list (there is no system trophy UI; seen on PPSA15552). +- [sceNpTrophy2RegisterContext, sceNpTrophy2DestroyHandle and sceNpTrophy2AbortHandle](../../../core/libs/prx/libSceNpTrophy2/src/Context.cpp) (libSceNpTrophy2) - return `SCE_NP_TROPHY2_OK` and ignore their arguments. Registering checks neither the context nor the handle and records nothing, and the handle `sceNpTrophy2CreateHandle` hands out (always 1) has nothing to destroy or abort. Eight titles in my set call the first two; Steins;Gate Re:Boot (PPSA36591) also calls `sceNpTrophy2AbortHandle`. ### Unknown function info From eced942005aa0ce7e3f606fd03023c006bfc1e62 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Tue, 27 Oct 2026 08:55:10 +0000 Subject: [PATCH 037/160] docs(libSceNpWebApi2): list the user-context and push-event teardown calls as silent stubs Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libSceNpWebApi2.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceNpWebApi2.md b/docs/dev/debt/libSceNpWebApi2.md index d033c1ede5..4a122ad7a0 100644 --- a/docs/dev/debt/libSceNpWebApi2.md +++ b/docs/dev/debt/libSceNpWebApi2.md @@ -4,6 +4,7 @@ - [sceNpWebApi2AddWebTraceTag](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - the `(request id, value)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api2/np_web_api2.cpp). A title imports it. It validates that the value pointer is not null (returning `INVALID_ARGUMENT` 0x80553402), accepts any request id, and stores nothing: no request reaches a service, as `sceNpWebApi2SendRequest` fails with `UNAVAILABLE` at once, so trace tags are not transmitted offline - [sceNpWebApi2PushEventRegisterCallback and sceNpWebApi2PushEventRegisterPushContextCallback](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - they store no callback and return a new id for any user context and filter, so an unknown context or filter is not rejected and no callback is ever called: no push event reaches a title, as network push operations are unimplemented and `sceNpWebApi2PushEventCreatePushContext` fails with `UNAVAILABLE`. The callback's prototype is not identified. The `(user context id, filter id, callback, user argument)` signature comes from one disassembled title (PPSA21564 01.007.000), not from a console: it calls them at 0x2ba5f0 and 0x2ba9e0 respectively, both with the id `sceNpWebApi2CreateUserContext` returned, the id `sceNpWebApi2PushEventCreateFilter` returned just before, the address of one of its functions and a user argument, and treats a negative result as failure +- [sceNpWebApi2DeleteUserContext, sceNpWebApi2PushEventDeleteFilter, sceNpWebApi2PushEventDeletePushContext, sceNpWebApi2PushEventUnregisterCallback, sceNpWebApi2PushEventUnregisterPushContextCallback and sceNpWebApi2PushEventStartPushContextCallback](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - return 0 for any id. `sceNpWebApi2CreateUserContext`, `sceNpWebApi2PushEventCreateFilter` and the callback registrations above hand out fresh ids without storing anything, so there is nothing to delete, unregister or start, and an unknown or already deleted id isn't rejected. Eight titles in my set delete a user context, and Borderlands 4 (PPSA01494), Ghost of Tsushima (PPSA02225), Astro Bot (PPSA21564), MXGP 26 (PPSA30839) and Super Meat Boy 3D (PPSA31162) also tear down filters, push contexts and callbacks. - [sceNpWebApi2SetRequestTimeout](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - the `(request id, timeout)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api2/np_web_api2.cpp). Pinball FX (PPSA08755) calls it while its main menu loads. It succeeds for any request id and stores nothing: no request reaches a service, as `sceNpWebApi2SendRequest` fails with `UNAVAILABLE` at once, so no timeout can expire. shadPS4 returns `LIB_CONTEXT_NOT_FOUND`, `USER_CONTEXT_NOT_FOUND` or `REQUEST_NOT_FOUND` for an id it does not know; this library keeps no context or request table (`sceNpWebApi2CreateRequest` hands out counter values, and `sceNpWebApi2AbortRequest`, `sceNpWebApi2AddHttpRequestHeader` and `sceNpWebApi2DeleteRequest` also accept any id), so those errors are not reported until the request functions track their ids together ### Unknown function info From a8657950bdc0f0dcab6a9f3e57da924eeffef58e Mon Sep 17 00:00:00 2001 From: PrivacyOnion Date: Tue, 27 Oct 2026 10:55:15 +0100 Subject: [PATCH 038/160] docs(debt): record silent stubs in libSceVoiceChat debt --- docs/dev/debt/libSceVoiceChat.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceVoiceChat.md b/docs/dev/debt/libSceVoiceChat.md index 98557e7eed..bf7419e63f 100644 --- a/docs/dev/debt/libSceVoiceChat.md +++ b/docs/dev/debt/libSceVoiceChat.md @@ -2,6 +2,7 @@ ### Silent stubs +- [sceVoiceChatDeleteRequest, sceVoiceChatInitialize, sceVoiceChatProcessEvent and sceVoiceChatTerminate](../../../core/libs/prx/libSceVoiceChat/Export.cpp) (libSceVoiceChat) - console behavior for these lifecycle, event, and request calls has not been measured; the current exports return success without managing sessions or requests, allowing offline titles and test suites to proceed without connecting to a voice chat daemon. - [sceVoiceChatAbortRequest, sceVoiceChatRegisterChannelEventHandler, sceVoiceChatRegisterDaemonEventHandler, sceVoiceChatRegisterMemberEventHandler, sceVoiceChatRegisterPartyUserStateChangedHandler, sceVoiceChatRegisterVoiceEventHandler, sceVoiceChatStartAccessibilityEvent, sceVoiceChatStartChannelEvent, sceVoiceChatStartDaemonEvent, sceVoiceChatStartMemberEvent, sceVoiceChatStartRangedMemberEvent, sceVoiceChatStartVoiceEvent, sceVoiceChatStopAccessibilityEvent, sceVoiceChatStopChannelEvent, sceVoiceChatStopDaemonEvent, sceVoiceChatStopMemberEvent, sceVoiceChatStopRangedMemberEvent, sceVoiceChatStopVoiceEvent, sceVoiceChatUnregisterAccessibilityEventHandler, sceVoiceChatUnregisterChannelEventHandler, sceVoiceChatUnregisterDaemonEventHandler, sceVoiceChatUnregisterMemberEventHandler, sceVoiceChatUnregisterMicEventHandler, sceVoiceChatUnregisterPartyUserStateChangedHandler, sceVoiceChatUnregisterRangedMemberEventHandler, sceVoiceChatUnregisterVoiceEventHandler](../../../core/libs/prx/libSceVoiceChat/Export.cpp) (libSceVoiceChat) - offline handler registration and event controls return 0 without connecting to a voice chat daemon, because the host environment does not run a PSN voice chat service; titles that register event listeners or start/stop audio capture events proceed safely without throwing - [sceVoiceChatRegisterHandlers and sceVoiceChatRegisterMicEventHandler](../../../core/libs/prx/libSceVoiceChat/Export.cpp) (libSceVoiceChat) - they keep no handler and return 0 without connecting to a voice chat daemon, so no handler is ever called; the handler table's layout and the handlers' prototypes are not identified. The signatures come from one disassembled title (PPSA21564 01.007.000), not from a console: it calls `sceVoiceChatRegisterHandlers` (0x2e473c) with a 0x30-byte structure of six function addresses and a user argument, then `sceVoiceChatRegisterMicEventHandler` (0x2e4751) with the 32-bit value it also passes as the user id of `sceNpWebApi2CreateUserContext`, a function address and the same user argument, and treats a negative result from either as failure From fd72671b7d4d886e056d6caada64ba0016ddee77 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Tue, 27 Oct 2026 16:42:16 +0000 Subject: [PATCH 039/160] docs(libSceNpUniversalDataSystem): link the handle teardown entry to Handle.cpp Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libSceNpUniversalDataSystem.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNpUniversalDataSystem.md b/docs/dev/debt/libSceNpUniversalDataSystem.md index dd9a81c121..e14389c0cb 100644 --- a/docs/dev/debt/libSceNpUniversalDataSystem.md +++ b/docs/dev/debt/libSceNpUniversalDataSystem.md @@ -2,7 +2,8 @@ ### Silent stubs -- [sceNpUniversalDataSystemRegisterContext, sceNpUniversalDataSystemDestroyHandle and sceNpUniversalDataSystemAbortHandle](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp) (libSceNpUniversalDataSystem) - return `SCE_NP_UNIVERSAL_DATA_SYSTEM_OK` without checking the context or handle and without recording anything. `sceNpUniversalDataSystemPostEvent` uses only its context and ignores the handle, so nothing depends on the registration. All ten titles in my set call the first two; Metal Gear Solid 4 (PPSA18194) also calls `sceNpUniversalDataSystemAbortHandle`. +- [sceNpUniversalDataSystemRegisterContext](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp) (libSceNpUniversalDataSystem) - returns `SCE_NP_UNIVERSAL_DATA_SYSTEM_OK` without checking the context or handle and without recording anything. `sceNpUniversalDataSystemPostEvent` uses only its context and ignores the handle, so nothing depends on the registration. All ten titles in my set call it. +- [sceNpUniversalDataSystemDestroyHandle and sceNpUniversalDataSystemAbortHandle](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Handle.cpp) (libSceNpUniversalDataSystem) - return `SCE_NP_UNIVERSAL_DATA_SYSTEM_OK` without checking the handle. `sceNpUniversalDataSystemCreateHandle` always hands out the same handle and keeps no state, so there is nothing to tear down. All ten titles in my set call `sceNpUniversalDataSystemDestroyHandle`; Metal Gear Solid 4 (PPSA18194) also calls `sceNpUniversalDataSystemAbortHandle`. ### Unknown function info From a9c75fe4087f32c62cb9f275da82c4b8276d54f7 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Tue, 27 Oct 2026 16:43:14 +0000 Subject: [PATCH 040/160] docs(libSceNpTrophy2): link the handle teardown entry to Handle.cpp Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libSceNpTrophy2.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNpTrophy2.md b/docs/dev/debt/libSceNpTrophy2.md index 223a7c3de1..ee6f4dc73c 100644 --- a/docs/dev/debt/libSceNpTrophy2.md +++ b/docs/dev/debt/libSceNpTrophy2.md @@ -4,7 +4,8 @@ - [sceNpTrophy2GetRewardIcon](../../../core/libs/prx/libSceNpTrophy2/src/Context.cpp) (libSceNpTrophy2) - zeroes size and returns `SCE_NP_TROPHY2_ERROR_ICON_FILE_NOT_FOUND` (-2141898479): the `(int context, int handle, int reward_id, void* buffer, size_t* size)` signature and two-stage query pattern follow the bundled `Media/Plugins/PSNCore.prx` of PPSA06888 and SharpProspero; returning -2141898479 (0x80553911) when icons are unavailable is inferred from sibling calls (sceNpTrophy2GetGameIcon, sceNpTrophy2GetGroupIcon, sceNpTrophy2GetTrophyIcon) in KytyPS5 (libTrophy.cpp:464/483/502) - [libSceNpTrophy2](../../../core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp): `sceNpTrophy2RegisterUnlockCallback` accepts a callback that is never called, as the callback's signature and when the console calls it are not known, `sceNpTrophy2UnregisterUnlockCallback` only returns success, and `sceNpTrophy2ShowTrophyList` returns success without showing a list (there is no system trophy UI; seen on PPSA15552). -- [sceNpTrophy2RegisterContext, sceNpTrophy2DestroyHandle and sceNpTrophy2AbortHandle](../../../core/libs/prx/libSceNpTrophy2/src/Context.cpp) (libSceNpTrophy2) - return `SCE_NP_TROPHY2_OK` and ignore their arguments. Registering checks neither the context nor the handle and records nothing, and the handle `sceNpTrophy2CreateHandle` hands out (always 1) has nothing to destroy or abort. Eight titles in my set call the first two; Steins;Gate Re:Boot (PPSA36591) also calls `sceNpTrophy2AbortHandle`. +- [sceNpTrophy2RegisterContext](../../../core/libs/prx/libSceNpTrophy2/src/Context.cpp) (libSceNpTrophy2) - returns `SCE_NP_TROPHY2_OK` and ignores its arguments: it checks neither the context nor the handle and records nothing. Eight titles in my set call it. +- [sceNpTrophy2DestroyHandle and sceNpTrophy2AbortHandle](../../../core/libs/prx/libSceNpTrophy2/src/Handle.cpp) (libSceNpTrophy2) - return `SCE_NP_TROPHY2_OK` and ignore the handle: `sceNpTrophy2CreateHandle` always hands out handle 1 and keeps no state, so there is nothing to destroy or abort. Eight titles in my set call `sceNpTrophy2DestroyHandle`; Steins;Gate Re:Boot (PPSA36591) also calls `sceNpTrophy2AbortHandle`. ### Unknown function info From dbce646e7bbdcbd8fbb3f4e35c5921c346f5bdab Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 08:53:29 +0000 Subject: [PATCH 041/160] fix(libc): keep guest environment strings alive across setenv and unsetenv setenv replaced a variable's string with a new allocation and freed the old one, and unsetenv freed it as well, so a pointer that getenv had returned for that variable dangled. A guest that reads such a pointer after setenv(name, value, 1) reads freed host memory: AddressSanitizer reports a heap-use-after-free on that read. FreeBSD 11.4's getenv.c, which holds the setenv, unsetenv and putenv the console's libc exports next to getenv, never frees a variable's string ("No previous variables are ever freed here to avoid causing a segmentation fault in a user's code", lib/libc/stdlib/getenv.c lines 449-520 at 8c639945a4). setenv and unsetenv mark the old string inactive; setenv writes the new value over the latest string of that name when the value fits and allocates a new string otherwise; putenv strings stay the caller's and are only forgotten. The guest environment now keeps every string it allocates with its value capacity and an active flag and follows that code: getenv and the time zone lookup read the name's latest active string, setenv reuses the latest string of the name when it fits, an unset one included, and otherwise appends a new one and deactivates the old, putenv replaces an earlier putenv string or deactivates the setenv one, and unsetenv deactivates them all. Built on the host next to this library, FreeBSD's getenv.c gave the same results, errno, pointer reuse and contents of every earlier pointer over 160,000 random calls once the putenv flag it leaves uninitialized in new setenv strings was cleared; the debt entry records that the console may allocate where this reuses. guest_environment checks the reuse, that earlier values stay readable after a longer setenv and after unsetenv, and that putenv and setenv leave each other's strings alone. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DXPnhogDW4zpeNCBro4Ctt --- core/libs/prx/libc/src/Environment.cpp | 94 ++++++++++++++++---------- core/libs/tests/GuestEnvironment.cpp | 16 +++++ docs/dev/debt/libc.md | 2 +- 3 files changed, 76 insertions(+), 36 deletions(-) diff --git a/core/libs/prx/libc/src/Environment.cpp b/core/libs/prx/libc/src/Environment.cpp index a04c872b46..b8fa65dc65 100644 --- a/core/libs/prx/libc/src/Environment.cpp +++ b/core/libs/prx/libc/src/Environment.cpp @@ -1,10 +1,11 @@ #include "prx/libc/include/Api.hpp" #include #include +#include #include -#include #include #include +#include #include #include #include @@ -13,15 +14,18 @@ namespace { struct EnvironmentEntry { - std::unique_ptr owned; + std::unique_ptr owned; char* borrowed = nullptr; - char* Data() { return borrowed ? borrowed : owned->data(); } - bool Matches(std::string_view name) { + std::size_t valueSize = 0; + bool active = true; + char* Data() const { return borrowed ? borrowed : owned.get(); } + bool Matches(std::string_view name) const { const char* data = Data(); const char* separator = std::strchr(data, '='); return separator && std::string_view(data, separator - data) == name; } }; +constexpr std::size_t missingEntry = static_cast(-1); std::mutex environmentMutex; std::vector& Environment() { static std::vector entries; @@ -31,28 +35,32 @@ bool ValidEnvironmentName(const char* name) { return name && *name && !std::strchr(name, '='); } int EnvironmentError(int error) { errno = error; return -1; } +std::size_t LatestEntry(std::string_view name, bool onlyActive) { + const auto& entries = Environment(); + for (std::size_t index = entries.size(); index-- > 0;) + if ((entries[index].active || !onlyActive) && entries[index].Matches(name)) return index; + return missingEntry; +} +char* ActiveValue(const char* name) { + const std::size_t found = LatestEntry(name, true); + return found == missingEntry ? nullptr : Environment()[found].Data() + std::strlen(name) + 1; +} } extern "C" { bool CopyGuestEnvironment_nid_no_patch(const char* name, std::string& value) { if (!ValidEnvironmentName(name)) return false; std::lock_guard lock(environmentMutex); - for (auto& entry : Environment()) - if (entry.Matches(name)) { - value = entry.Data() + std::strlen(name) + 1; - return true; - } - return false; + const char* found = ActiveValue(name); + if (!found) return false; + value = found; + return true; } char* APS5_VABI getenv_nid_postfix(const char* name) { if (!ValidEnvironmentName(name)) { errno = 22; return nullptr; } - try { - std::lock_guard lock(environmentMutex); - for (auto& entry : Environment()) - if (entry.Matches(name)) return entry.Data() + std::strlen(name) + 1; - return nullptr; - } catch (const std::bad_alloc&) { errno = 12; return nullptr; } + std::lock_guard lock(environmentMutex); + return ActiveValue(name); } int APS5_VABI setenv_nid_postfix(const char* name, const char* value, int overwrite) { @@ -60,25 +68,40 @@ int APS5_VABI setenv_nid_postfix(const char* name, const char* value, int overwr try { std::lock_guard lock(environmentMutex); auto& entries = Environment(); - auto found = std::find_if(entries.begin(), entries.end(), - [name](auto& entry) { return entry.Matches(name); }); - if (found != entries.end() && !overwrite) return 0; - EnvironmentEntry replacement{std::make_unique(std::string(name) + '=' + value), nullptr}; - if (found != entries.end()) *found = std::move(replacement); - else entries.push_back(std::move(replacement)); + const std::size_t nameLength = std::strlen(name); + const std::size_t valueLength = std::strlen(value); + const std::size_t found = LatestEntry(name, false); + if (found != missingEntry) { + auto& entry = entries[found]; + if (entry.active && !overwrite) return 0; + if (!entry.borrowed && entry.valueSize >= valueLength) { + std::memmove(entry.owned.get() + nameLength + 1, value, valueLength + 1); + entry.active = true; + return 0; + } + } + EnvironmentEntry created{std::make_unique(nameLength + valueLength + 2), nullptr, valueLength}; + std::memcpy(created.owned.get(), name, nameLength); + created.owned[nameLength] = '='; + std::memcpy(created.owned.get() + nameLength + 1, value, valueLength + 1); + entries.push_back(std::move(created)); + if (found != missingEntry) { + if (entries[found].borrowed) entries.erase(entries.begin() + static_cast(found)); + else entries[found].active = false; + } return 0; } catch (const std::bad_alloc&) { return EnvironmentError(12); } } int APS5_VABI unsetenv_nid_postfix(const char* name) { if (!ValidEnvironmentName(name)) return EnvironmentError(22); - try { - std::lock_guard lock(environmentMutex); - auto& entries = Environment(); - const std::string key(name); - std::erase_if(entries, [&key](auto& entry) { return entry.Matches(key); }); - return 0; - } catch (const std::bad_alloc&) { return EnvironmentError(12); } + std::lock_guard lock(environmentMutex); + auto& entries = Environment(); + const std::string_view key(name); + std::erase_if(entries, [key](const EnvironmentEntry& entry) { return entry.borrowed && entry.Matches(key); }); + for (auto& entry : entries) + if (entry.Matches(key)) entry.active = false; + return 0; } int APS5_VABI putenv_nid_postfix(char* text) { @@ -87,12 +110,13 @@ int APS5_VABI putenv_nid_postfix(char* text) { try { std::lock_guard lock(environmentMutex); auto& entries = Environment(); - const std::string_view name(text, separator - text); - auto found = std::find_if(entries.begin(), entries.end(), - [name](auto& entry) { return entry.Matches(name); }); - EnvironmentEntry replacement{nullptr, text}; - if (found != entries.end()) *found = std::move(replacement); - else entries.push_back(std::move(replacement)); + const std::size_t found = LatestEntry(std::string_view(text, separator - text), true); + if (found != missingEntry && entries[found].borrowed) { + entries[found].borrowed = text; + return 0; + } + entries.push_back({nullptr, text}); + if (found != missingEntry) entries[found].active = false; return 0; } catch (const std::bad_alloc&) { return EnvironmentError(12); } } diff --git a/core/libs/tests/GuestEnvironment.cpp b/core/libs/tests/GuestEnvironment.cpp index aab45e4742..2bb0be7088 100644 --- a/core/libs/tests/GuestEnvironment.cpp +++ b/core/libs/tests/GuestEnvironment.cpp @@ -60,4 +60,20 @@ int main() { for (int index = 0; index < 64; ++index) Require(setenv_nid_postfix(("ANYPS5_GUEST_ENV_GROW_4C27_" + std::to_string(index)).c_str(), "x", 1) == 0); Require(getenv_nid_postfix("Q7B") == kept && std::strcmp(kept, "b") == 0); + Require(setenv_nid_postfix("Q7C", "abc", 1) == 0); + char* const first = getenv_nid_postfix("Q7C"); + Require(setenv_nid_postfix("Q7C", "xy", 1) == 0); + Require(getenv_nid_postfix("Q7C") == first && std::strcmp(first, "xy") == 0); + Require(setenv_nid_postfix("Q7C", "longer value", 1) == 0); + char* const second = getenv_nid_postfix("Q7C"); + Require(second != first && std::strcmp(second, "longer value") == 0 && std::strcmp(first, "xy") == 0); + Require(unsetenv_nid_postfix("Q7C") == 0); + Require(getenv_nid_postfix("Q7C") == nullptr && std::strcmp(second, "longer value") == 0); + Require(setenv_nid_postfix("Q7C", "again", 0) == 0); + Require(getenv_nid_postfix("Q7C") == second && std::strcmp(second, "again") == 0); + char lent[] = "Q7C=lent"; + Require(putenv_nid_postfix(lent) == 0); + Require(getenv_nid_postfix("Q7C") == lent + 4 && std::strcmp(second, "again") == 0); + Require(setenv_nid_postfix("Q7C", "own", 1) == 0); + Require(std::strcmp(getenv_nid_postfix("Q7C"), "own") == 0 && std::strcmp(lent, "Q7C=lent") == 0 && std::strcmp(second, "again") == 0); } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index d272adb019..d71ff1bbf9 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -137,7 +137,7 @@ - [asinf](../../../core/libs/prx/libc/src/Math.cpp) (libc) - for |x| > 1, the infinities and NaN, it stores `EDOM` before returning `(x - x) / (x - x)`, as the `libc.prx` bundled with eleven titles does, PPSA13934's included. Tiny arguments return `x` without raising `FE_INEXACT`. Otherwise this is FreeBSD msun's [`e_asinf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_asinf.c), which gives the same bits as the `asinf` of the `libc.prx` bundled with PPSA07923 and PPSA05916 for all 2^32 arguments (run on a Windows host). The FMA-contracted builds bundled with PPSA13934 and PPSA09614 differ from it by 1 ulp on 391,714 arguments, so no single port matches every title's last bit. - [atanhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `atanhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_atanhf.c`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_atanhf.c) calling msun's [`log1pf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_log1pf.c), whose paths for the positive arguments it passes are inlined here; the exported `log1pf` is still the host's. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, and for |x| = 1 it raises `FE_DIVBYZERO` and sets `errno` to `ERANGE` before returning `x / 0`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits, `errno` and floating-point exceptions as this implementation for all 2^32 arguments; it was not run on Linux. That the system `libSceLibcInternal`, which Dusklight (PPSA99305) imports while bundling a zero-return `libc.prx`, has the same code is assumed - [j0, j1, jn, y0, y1 and yn](../../../core/libs/prx/libc/src/Bessel.cpp) (libc) call the host's `sin`, `cos`, `sincos` and `log`, so the last bits can differ from the console. The rest follows the system `libc.prx` built as `J03093718` (disassembly), which given the console's own `sin`, `cos`, `sincos` and `log` gave the same bits and exception flags for 90 million arguments: msun's [`e_j0.c`, `e_j1.c` and `e_jn.c`](https://github.com/freebsd/freebsd-src/tree/release/10.0.0/lib/msun/src) with each multiply-add the console fuses done as one FMA. As in that release, before FreeBSD commit `186f620727`, `y0`, `y1` and `yn` return `-inf` for ±0 and `0x7ff8000000000000` for a negative argument without raising an exception. The order wraps at 32 bits as on the console: `jn(1 << 30, x)` never returns for 2^-29 <= |x| < 2^30 -- [getenv, setenv, unsetenv, putenv](../../../core/libs/prx/libc/src/Environment.cpp) (libc) keep one guest environment that starts empty, like the `environ` libkernel exports, and never reads or changes the host's. A title sees only the variables it sets itself; host variables such as `HOME` and `PATH` name host paths a guest cannot use (PS5MC, PPSA99420, exits with `XBMC_Run returned -1` when it gets the host's `HOME`). Whether the console starts a title with any variables set is not measured +- [getenv, setenv, unsetenv, putenv](../../../core/libs/prx/libc/src/Environment.cpp) (libc) keep one guest environment that starts empty, like the `environ` libkernel exports, and never reads or changes the host's. A title sees only the variables it sets itself; host variables such as `HOME` and `PATH` name host paths a guest cannot use (PS5MC, PPSA99420, exits with `XBMC_Run returned -1` when it gets the host's `HOME`). Whether the console starts a title with any variables set is not measured. Value storage follows FreeBSD 11.4 [getenv.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getenv.c#L449-L520), not measured on a console: no string is freed, so a pointer `getenv` returned stays readable after `setenv` or `unsetenv`, and `setenv` writes a value that fits over the latest string of that name. FreeBSD bases that reuse on a flag it leaves uninitialized in strings `setenv` adds, so the console may allocate where this reuses - [mergesort](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) checks its arguments like the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x762D0): an element size under 4 sets `errno` to `EINVAL` (22) and returns -1, an empty array returns 0, and a failed `malloc` of the count times the size plus 8 bytes returns -1. The sort is a stable bottom-up merge, not FreeBSD's natural merge with its insertion sort for short runs, so the comparator is called in a different order and number of times; the result is the same for a consistent comparator. A null base or comparator and a size product that overflows, which the console does not check, throw - [Exception runtime on macOS](../../../core/libs/prx/libc/src/exception/Exports.cpp) (libc) - on macOS an exception that a library throws with a plain `throw` and that nothing catches skips the guest's `std::set_terminate` handler and the guest runtime's report: it ends in libc++abi's terminate, because the host keeps libc++abi there, while on Linux and Windows libc's runtime takes these throws. The `SIGALRM` expiry of `alarm` and `setitimer` throws through the guest runtime on macOS; `NotImplemented_nid_no_patch` cannot, since host code catches its `std::runtime_error`, so an unimplemented call inside a `noexcept` export (`mmap` on an `app0.zar` file) bypasses the guest handler - On macOS the [write watch](../../../core/libs/prx/libc/src/GuestWriteWatch.cpp) write-protects watched pages and records the first write to each from a SIGBUS/SIGSEGV handler, because macOS has no counterpart to `userfaultfd` write-protect or `MEM_WRITE_WATCH`. The protection is real, so: From 76c419a5728dd4b4b992636a50b5fb7d8ab6be30 Mon Sep 17 00:00:00 2001 From: "husari.orozco" Date: Tue, 27 Oct 2026 12:32:07 -0600 Subject: [PATCH 042/160] fix(tests): name the raise-exception test's signal so macOS builds it The test declared its own `static constexpr int SIGUSR1 = 30;`, and the macOS SDK defines `SIGUSR1` as a macro in , which libc++ reaches through and . The declaration then expands to `static constexpr int 30 = 30;` and the translation unit does not compile, so `guest_raise_exception_tests` and every full macOS build with BUILD_TESTING=ON stop there. Rename it to GUEST_SIGUSR1, the name the other signal tests already use. The value and every use stay the same. --- core/libs/tests/GuestRaiseException.cpp | 56 ++++++++++++------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/core/libs/tests/GuestRaiseException.cpp b/core/libs/tests/GuestRaiseException.cpp index 439e808613..deffa7dac0 100644 --- a/core/libs/tests/GuestRaiseException.cpp +++ b/core/libs/tests/GuestRaiseException.cpp @@ -18,7 +18,7 @@ #include #endif -static constexpr int SIGUSR1 = 30; +static constexpr int GUEST_SIGUSR1 = 30; static constexpr int Repeats = 50; static constexpr std::size_t R12Offset = 0xa0; static constexpr std::size_t R13Offset = 0xa8; @@ -45,7 +45,7 @@ static std::atomic observedXmm8{0}; static std::atomic observedMxcsr{0}; static void APS5_VABI Handler(int signum, void* context) { - Require(signum == SIGUSR1); + Require(signum == GUEST_SIGUSR1); auto* bytes = static_cast(context); std::uint64_t rsp = 0; std::memcpy(&rsp, bytes + 0xf8, sizeof(rsp)); @@ -301,7 +301,7 @@ static std::atomic nestedResult{0}; static std::atomic nestedCalls{0}; static void APS5_VABI NestedHandler(int signum, void*) { - Require(signum == SIGUSR1); + Require(signum == GUEST_SIGUSR1); nestedEntered.store(true); KernelUseconds timeout = nestedTimeout; nestedResult.store(sceKernelSyncOnAddressWait(&nestedWord, 0, &timeout, "nested")); @@ -353,7 +353,7 @@ static void NestedWaitKeepsLaterWaiters() { StartSemaWaiter(&secondThread, second, sem, "nested second"); nestedTimeout = 20000; const int before = nestedCalls.load(); - Require(sceKernelRaiseException(firstThread, SIGUSR1) == 0); + Require(sceKernelRaiseException(firstThread, GUEST_SIGUSR1) == 0); ExpectNestedDone(before); Require(sceKernelSignalSema(sem, 2) == 0); ExpectReturned(first); @@ -372,7 +372,7 @@ static void NestedWaitKeepsOuterWake() { nestedTimeout = 1000000; nestedEntered.store(false); const int before = nestedCalls.load(); - Require(sceKernelRaiseException(thread, SIGUSR1) == 0); + Require(sceKernelRaiseException(thread, GUEST_SIGUSR1) == 0); while (!nestedEntered.load()) std::this_thread::yield(); std::this_thread::sleep_for(std::chrono::milliseconds(50)); Require(sceKernelSignalSema(sem, 1) == 0); @@ -391,7 +391,7 @@ static std::atomic holdingLeft{false}; static std::atomic holdingThread; static void APS5_VABI HoldingHandler(int signum, void*) { - Require(signum == SIGUSR1); + Require(signum == GUEST_SIGUSR1); holdingThread.store(std::this_thread::get_id()); holdingEntered.store(true); while (!holdingRelease.load()) std::this_thread::yield(); @@ -443,7 +443,7 @@ static void HandlerLeavesAddressLock(PthreadEntry entry, const char* name) { holdingEntered.store(false); holdingLeft.store(false); holdingRelease.store(false); - RequireInRound(sceKernelRaiseException(thread, SIGUSR1) == 0, name, round, "the raise failed"); + RequireInRound(sceKernelRaiseException(thread, GUEST_SIGUSR1) == 0, name, round, "the raise failed"); RequireInRound(AwaitWithin(holdingEntered, std::chrono::seconds(5)), name, round, "the handler did not run within 5 s"); RequireInRound(holdingThread.load() == worker.id, name, round, "the handler ran on another thread"); std::atomic woke{false}; @@ -480,7 +480,7 @@ static void HandlerRunsInUnboundedAddressWait() { holdingEntered.store(false); holdingLeft.store(false); holdingRelease.store(false); - Require(sceKernelRaiseException(thread, SIGUSR1) == 0); + Require(sceKernelRaiseException(thread, GUEST_SIGUSR1) == 0); RequireInRound(AwaitWithin(holdingEntered, std::chrono::seconds(5)), "address idle", 0, "the handler did not run while the thread waited without a timeout"); RequireInRound(holdingThread.load() == worker.id, "address idle", 0, "the handler ran on another thread"); holdingRelease.store(true); @@ -540,7 +540,7 @@ static void ExpectDeliveryWhileLockHeld(int (*lock)(), int (*unlock)(), int befo for (int round = 1; round <= LockRounds; ++round) { while (worker.blocking.load() != round) std::this_thread::yield(); std::this_thread::sleep_for(std::chrono::milliseconds(20)); - Require(sceKernelRaiseException(thread, SIGUSR1) == 0); + Require(sceKernelRaiseException(thread, GUEST_SIGUSR1) == 0); ExpectDelivery(before + round - 1, worker.id); Require(worker.acquired.load() == round - 1); Require(unlock() == 0); @@ -556,15 +556,15 @@ int main() { Require(sceKernelRaiseException(scePthreadSelf(), 11) == SCE_KERNEL_ERROR_EINVAL); bool rejected = false; try { - sceKernelRaiseException(scePthreadSelf(), SIGUSR1); + sceKernelRaiseException(scePthreadSelf(), GUEST_SIGUSR1); } catch (const std::runtime_error&) { rejected = true; } Require(rejected); - Require(sceKernelInstallExceptionHandler(SIGUSR1, reinterpret_cast(&Handler)) == 0); + Require(sceKernelInstallExceptionHandler(GUEST_SIGUSR1, reinterpret_cast(&Handler)) == 0); - Require(sceKernelRaiseException(scePthreadSelf(), SIGUSR1) == 0); + Require(sceKernelRaiseException(scePthreadSelf(), GUEST_SIGUSR1) == 0); Require(calls.load() == 1 && handlerThread.load() == std::this_thread::get_id()); Worker busy; @@ -572,7 +572,7 @@ int main() { Require(scePthreadCreate(&busyThread, nullptr, Busy, &busy, "busy") == 0); while (!busy.started.load()) std::this_thread::yield(); for (int raised = 0; raised < Repeats; ++raised) { - Require(sceKernelRaiseException(busyThread, SIGUSR1) == 0); + Require(sceKernelRaiseException(busyThread, GUEST_SIGUSR1) == 0); ExpectDelivery(1 + raised, busy.id); } busy.stop.store(true); @@ -585,7 +585,7 @@ int main() { while (!waiting.started.load()) std::this_thread::yield(); std::this_thread::sleep_for(std::chrono::milliseconds(50)); for (int raised = 0; raised < Repeats; ++raised) { - Require(sceKernelRaiseException(waitingThread, SIGUSR1) == 0); + Require(sceKernelRaiseException(waitingThread, GUEST_SIGUSR1) == 0); ExpectDelivery(1 + Repeats + raised, waiting.id); } Require(sceKernelSignalSema(waiting.sem, 1) == 0); @@ -600,7 +600,7 @@ int main() { for (int round = 0; round < HostRounds; ++round) { while (!blocked.started.load()) std::this_thread::yield(); std::this_thread::sleep_for(std::chrono::milliseconds(20)); - Require(sceKernelRaiseException(blockedThread, SIGUSR1) == 0); + Require(sceKernelRaiseException(blockedThread, GUEST_SIGUSR1) == 0); hostLock.unlock(); ExpectDelivery(1 + 2 * Repeats + round, blocked.id); while (hostAcquired.load() != round + 1) std::this_thread::yield(); @@ -622,7 +622,7 @@ int main() { Require(sceKernelSignalSema(leaving.sem, 1) == 0); const auto offset = std::chrono::steady_clock::now() + std::chrono::microseconds(round % 16 * 8); while (std::chrono::steady_clock::now() < offset) {} - Require(sceKernelRaiseException(leavingThread, SIGUSR1) == 0); + Require(sceKernelRaiseException(leavingThread, GUEST_SIGUSR1) == 0); ExpectDelivery(1 + 2 * Repeats + HostRounds + round, leaving.id); leavingRound.store(round + 1); } @@ -637,7 +637,7 @@ int main() { Require(scePthreadCreate(&continuingThread, nullptr, Continuing, &continuing, "continuing") == 0); while (!continuing.started.load() || continued.load() == 0) std::this_thread::yield(); for (int raised = 0; raised < ContinuingRounds; ++raised) { - Require(sceKernelRaiseException(continuingThread, SIGUSR1) == 0); + Require(sceKernelRaiseException(continuingThread, GUEST_SIGUSR1) == 0); ExpectDelivery(1 + 2 * Repeats + HostRounds + LeavingRounds + raised, continuing.id); } continuing.stop.store(true); @@ -649,7 +649,7 @@ int main() { Require(scePthreadCreate(&selfingThread, nullptr, Selfing, &selfing, "selfing") == 0); while (!selfing.started.load()) std::this_thread::yield(); for (int raised = 0; raised < SelfingRounds; ++raised) { - Require(sceKernelRaiseException(selfingThread, SIGUSR1) == 0); + Require(sceKernelRaiseException(selfingThread, GUEST_SIGUSR1) == 0); ExpectDelivery(1 + 2 * Repeats + HostRounds + LeavingRounds + ContinuingRounds + raised, selfing.id); Require(handlerSelf.load() == selfingThread); } @@ -665,7 +665,7 @@ int main() { const auto raisedAt = std::chrono::steady_clock::now(); bool refused = false; try { - sceKernelRaiseException(loopingThread, SIGUSR1); + sceKernelRaiseException(loopingThread, GUEST_SIGUSR1); } catch (const std::runtime_error&) { refused = true; @@ -693,7 +693,7 @@ int main() { while (!vectors.started.load()) std::this_thread::yield(); std::this_thread::sleep_for(std::chrono::milliseconds(20)); for (int raised = 0; raised < VectorRounds; ++raised) { - Require(sceKernelRaiseException(vectorsThread, SIGUSR1) == 0); + Require(sceKernelRaiseException(vectorsThread, GUEST_SIGUSR1) == 0); ExpectDelivery(1 + 2 * Repeats + HostRounds + LeavingRounds + ContinuingRounds + SelfingRounds + raised, vectors.id); } clobberVectors.store(false); @@ -709,7 +709,7 @@ int main() { while (spin.started.load() == 0) std::this_thread::yield(); patchContext.store(true); const int spinBefore = calls.load(); - Require(sceKernelRaiseException(spinningThread, SIGUSR1) == 0); + Require(sceKernelRaiseException(spinningThread, GUEST_SIGUSR1) == 0); ExpectDelivery(spinBefore, spinningId.load()); Require(scePthreadJoin(spinningThread, nullptr) == 0); patchContext.store(false); @@ -722,7 +722,7 @@ int main() { startingRelease.store(false); Pthread startingThread = nullptr; Require(scePthreadCreate(&startingThread, nullptr, Starting, nullptr, "starting") == 0); - Require(sceKernelRaiseException(startingThread, SIGUSR1) == 0); + Require(sceKernelRaiseException(startingThread, GUEST_SIGUSR1) == 0); for (int attempt = 0; attempt < 5000 && calls.load() == before; ++attempt) std::this_thread::sleep_for(std::chrono::milliseconds(1)); Require(calls.load() == before + 1); Require(handlerSelf.load() == startingThread); @@ -743,19 +743,19 @@ int main() { Require(scePthreadCreate(&finishedThread, nullptr, Finished, nullptr, "finished") == 0); while (!finishedReturned.load()) std::this_thread::yield(); std::this_thread::sleep_for(std::chrono::milliseconds(50)); - Require(sceKernelRaiseException(finishedThread, SIGUSR1) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelRaiseException(finishedThread, GUEST_SIGUSR1) == SCE_KERNEL_ERROR_ESRCH); Require(scePthreadJoin(finishedThread, nullptr) == 0); - Require(sceKernelRemoveExceptionHandler(SIGUSR1) == 0); + Require(sceKernelRemoveExceptionHandler(GUEST_SIGUSR1) == 0); - Require(sceKernelInstallExceptionHandler(SIGUSR1, reinterpret_cast(&NestedHandler)) == 0); + Require(sceKernelInstallExceptionHandler(GUEST_SIGUSR1, reinterpret_cast(&NestedHandler)) == 0); NestedWaitKeepsLaterWaiters(); NestedWaitKeepsOuterWake(); - Require(sceKernelRemoveExceptionHandler(SIGUSR1) == 0); + Require(sceKernelRemoveExceptionHandler(GUEST_SIGUSR1) == 0); - Require(sceKernelInstallExceptionHandler(SIGUSR1, reinterpret_cast(&HoldingHandler)) == 0); + Require(sceKernelInstallExceptionHandler(GUEST_SIGUSR1, reinterpret_cast(&HoldingHandler)) == 0); HandlerLeavesAddressLock(AddressWaking, "address waking"); HandlerLeavesAddressLock(AddressWaiting, "address waiting"); HandlerRunsInUnboundedAddressWait(); - Require(sceKernelRemoveExceptionHandler(SIGUSR1) == 0); + Require(sceKernelRemoveExceptionHandler(GUEST_SIGUSR1) == 0); } From c79e1d3a9c6d49e51cdeb2023272351ce6c7194b Mon Sep 17 00:00:00 2001 From: Nicolas Gonzalez Date: Tue, 27 Oct 2026 22:44:03 -0300 Subject: [PATCH 043/160] fix(libc): list getifaddrs AF_LINK entries on macOS hosts getifaddrs listed each interface's AF_LINK entry only on Linux. On macOS the entry now comes from the host's own AF_LINK sockaddr_dl and if_data: index, type (macOS and FreeBSD share the IFT_ numbers), hardware address, header length, MTU, metric, baud rate and counters, with the link state from SIOCGIFMEDIA, or from IFF_UP and IFF_RUNNING for an interface without media. The interfaces test checks the link-level entries against the host's on macOS too. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Interfaces.cpp | 58 ++++++++++++++++++++++++++- core/libs/tests/GuestInterfaces.cpp | 50 +++++++++++++++++++++-- docs/dev/debt/libc.md | 2 +- 3 files changed, 103 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libc/src/Interfaces.cpp b/core/libs/prx/libc/src/Interfaces.cpp index f9ec682ec7..bbfee667a9 100644 --- a/core/libs/prx/libc/src/Interfaces.cpp +++ b/core/libs/prx/libc/src/Interfaces.cpp @@ -12,6 +12,9 @@ #include #include #include +#elif defined(__APPLE__) +#include +#include #endif #include #include @@ -168,7 +171,7 @@ int HostEntries(std::vector& entries) { return 0; } #else -#ifdef __linux__ +#if defined(__linux__) || defined(__APPLE__) constexpr std::uint8_t guestLinkFamily = 18; constexpr std::size_t guestLinkNameArea = 8 + 16; constexpr std::size_t guestLinkMinimum = 54; @@ -179,6 +182,8 @@ constexpr std::uint8_t guestTypeLoopback = 0x18; constexpr std::uint8_t guestLinkStateDown = 1; constexpr std::uint8_t guestLinkStateUp = 2; constexpr std::size_t guestInterfaceDataSize = 152; +#endif +#ifdef __linux__ constexpr unsigned int hostLowerUp = 0x10000; #endif @@ -189,7 +194,7 @@ struct HostInterface { std::vector addresses; }; -#ifdef __linux__ +#if defined(__linux__) || defined(__APPLE__) template void Store(std::vector& bytes, std::size_t offset, TValue value) { std::memcpy(bytes.data() + offset, &value, sizeof(value)); @@ -210,7 +215,9 @@ std::vector GuestLinkAddress(const std::string& name, std::uint16_ if (hardwareLength != 0) std::memcpy(guest.data() + 8 + name.size(), hardware, hardwareLength); return guest; } +#endif +#ifdef __linux__ std::optional ReadSysfsCounter(const std::string& name, const char* file) { std::ifstream input("/sys/class/net/" + name + "/" + file); long long value = 0; @@ -264,6 +271,39 @@ std::vector GuestInterfaceData(const std::string& name, unsigned i } return data; } +#elif defined(__APPLE__) +bool HostLinkActive(const std::string& name, unsigned int hostFlags) { + const int probe = ::socket(AF_INET, SOCK_DGRAM, 0); + if (probe < 0) throw std::runtime_error(std::string("getifaddrs: no socket to read the media status of ") + name + ": " + std::strerror(errno)); + ifmediareq request{}; + std::memcpy(request.ifm_name, name.c_str(), name.size() + 1); + const int result = ::ioctl(probe, SIOCGIFMEDIA, &request); + ::close(probe); + if (result == 0 && (request.ifm_status & IFM_AVALID)) return (request.ifm_status & IFM_ACTIVE) != 0; + return (hostFlags & IFF_UP) && (hostFlags & IFF_RUNNING); +} + +std::vector GuestInterfaceData(const std::string& name, unsigned int hostFlags, const void* hostData, std::uint8_t type, std::uint8_t hardwareLength) { + if (!hostData) throw std::runtime_error("getifaddrs: no if_data for " + name + " in the host list"); + if_data host{}; + std::memcpy(&host, hostData, sizeof(host)); + std::vector data(guestInterfaceDataSize, 0); + data[0] = type; + data[2] = hardwareLength; + data[3] = type == guestTypeLoopback ? 0 : host.ifi_hdrlen; + if (type == guestTypeLoopback) data[4] = (hostFlags & IFF_UP) ? guestLinkStateUp : guestLinkStateDown; + else data[4] = HostLinkActive(name, hostFlags) ? guestLinkStateUp : guestLinkStateDown; + Store(data, 6, static_cast(guestInterfaceDataSize)); + Store(data, 8, static_cast(host.ifi_mtu)); + Store(data, 12, static_cast(host.ifi_metric)); + Store(data, 16, static_cast(host.ifi_baudrate)); + const std::pair counters[] = { + {host.ifi_ipackets, 24}, {host.ifi_ierrors, 32}, {host.ifi_opackets, 40}, {host.ifi_oerrors, 48}, {host.ifi_collisions, 56}, + {host.ifi_ibytes, 64}, {host.ifi_obytes, 72}, {host.ifi_imcasts, 80}, {host.ifi_omcasts, 88}, {host.ifi_iqdrops, 96}, {host.ifi_noproto, 112}, + }; + for (const auto& [value, offset] : counters) Store(data, offset, static_cast(value)); + return data; +} #endif std::uint32_t InterfaceFlags(unsigned int host) { @@ -315,6 +355,20 @@ int HostEntries(std::vector& entries) { owner.linked = true; continue; } +#elif defined(__APPLE__) + if (item->ifa_addr->sa_family == AF_LINK) { + const auto& link = *reinterpret_cast(item->ifa_addr); + HostInterface& owner = interfaceFor(CheckedName(item->ifa_name)); + if (owner.linked) throw std::runtime_error("getifaddrs: two link-level entries for " + owner.name); + if (link.sdl_index == 0) throw std::runtime_error("getifaddrs: link-level entry of " + owner.name + " has no interface index"); + const std::uint8_t type = link.sdl_type; + const std::uint8_t hardwareLength = type == guestTypeLoopback ? 0 : link.sdl_alen; + owner.link = Entry{owner.name, InterfaceFlags(item->ifa_flags), + GuestLinkAddress(owner.name, link.sdl_index, type, reinterpret_cast(link.sdl_data + link.sdl_nlen), hardwareLength), {}, {}, + GuestInterfaceData(owner.name, item->ifa_flags, item->ifa_data, type, hardwareLength)}; + owner.linked = true; + continue; + } #endif if (item->ifa_addr->sa_family != AF_INET && item->ifa_addr->sa_family != AF_INET6) continue; if (!item->ifa_netmask) throw std::runtime_error(std::string("getifaddrs: host address without a netmask on ") + item->ifa_name); diff --git a/core/libs/tests/GuestInterfaces.cpp b/core/libs/tests/GuestInterfaces.cpp index 6306850521..47ee2a196b 100644 --- a/core/libs/tests/GuestInterfaces.cpp +++ b/core/libs/tests/GuestInterfaces.cpp @@ -12,6 +12,8 @@ #ifdef __linux__ #include #include +#elif defined(__APPLE__) +#include #endif #include #include @@ -21,6 +23,7 @@ #endif #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -64,12 +67,36 @@ static HostInterfaceAddress HostAddress(const sockaddr* address) { } #ifdef __linux__ +constexpr int HostLinkFamily = AF_PACKET; + static std::uint64_t SysfsCounter(const char* name, const char* file) { std::ifstream input(std::string("/sys/class/net/") + name + "/statistics/" + file); unsigned long long value = 0; Require(static_cast(input >> value)); return value; } +#elif defined(__APPLE__) +constexpr int HostLinkFamily = AF_LINK; + +static std::uint64_t SysfsCounter(const char* name, const char* file) { + ifaddrs* host = nullptr; + Require(::getifaddrs(&host) == 0); + std::uint64_t value = 0; + bool found = false; + for (auto* item = host; item; item = item->ifa_next) { + if (!item->ifa_addr || item->ifa_addr->sa_family != AF_LINK || std::strcmp(item->ifa_name, name) != 0 || !item->ifa_data) continue; + const auto& data = *static_cast(item->ifa_data); + const std::string counter(file); + value = counter == "rx_packets" ? data.ifi_ipackets : counter == "tx_packets" ? data.ifi_opackets : counter == "rx_bytes" ? data.ifi_ibytes : data.ifi_obytes; + found = true; + } + ::freeifaddrs(host); + Require(found); + return value; +} +#endif + +#if defined(__linux__) || defined(__APPLE__) static std::uint32_t HostMtu(const char* name) { const int probe = ::socket(AF_INET, SOCK_DGRAM, 0); @@ -153,6 +180,21 @@ static std::vector HostSnapshot() { members.insert(members.begin(), entry); continue; } +#elif defined(__APPLE__) + if (item->ifa_addr->sa_family == AF_LINK) { + const auto& link = *reinterpret_cast(item->ifa_addr); + HostInterfaceAddress entry{}; + entry.family = AF_LINK; + std::strncpy(entry.name, item->ifa_name, sizeof(entry.name) - 1); + entry.index = link.sdl_index; + entry.type = link.sdl_type; + entry.hardwareLength = entry.type == 0x18 ? 0 : std::min(link.sdl_alen, sizeof(entry.hardware)); + std::memcpy(entry.hardware, link.sdl_data + link.sdl_nlen, entry.hardwareLength); + entry.loopbackUp = (item->ifa_flags & IFF_UP) != 0; + auto& members = group(item->ifa_name); + members.insert(members.begin(), entry); + continue; + } #endif if (item->ifa_addr->sa_family != AF_INET && item->ifa_addr->sa_family != AF_INET6) continue; Require(item->ifa_netmask != nullptr); @@ -181,7 +223,7 @@ int main() { const std::uint8_t loopbackAddress[] = {127, 0, 0, 1}; const std::uint8_t loopbackMask[] = {255, 0, 0, 0}; bool matched = false; -#ifdef __linux__ +#if defined(__linux__) || defined(__APPLE__) bool linkChecked = false; SendLoopbackDatagram(); #endif @@ -200,7 +242,7 @@ int main() { Require((item->flags & ~knownFlags) == 0); const auto* address = static_cast(item->address); Require(address != nullptr && reinterpret_cast(address) % 8 == 0); -#ifdef __linux__ +#if defined(__linux__) || defined(__APPLE__) if (address[1] == 18) { Require(item->netmask == nullptr && item->destination == nullptr); const auto* data = static_cast(item->data); @@ -223,7 +265,7 @@ int main() { Require(counters[5] <= SysfsCounter(item->name, "rx_bytes") && counters[6] <= SysfsCounter(item->name, "tx_bytes")); if (entries <= host.size()) { const HostInterfaceAddress& expected = host[entries - 1]; - equal = equal && expected.family == AF_PACKET && std::strcmp(expected.name, item->name) == 0 && expected.index == index && + equal = equal && expected.family == HostLinkFamily && std::strcmp(expected.name, item->name) == 0 && expected.index == index && expected.type == address[4] && expected.hardwareLength == address[6] && std::memcmp(expected.hardware, address + 8 + nameLength, expected.hardwareLength) == 0; if (expected.type == 0x18) { @@ -285,7 +327,7 @@ int main() { } Require(matched); -#ifdef __linux__ +#if defined(__linux__) || defined(__APPLE__) Require(linkChecked); #endif GuestInterfaceAddress* released = nullptr; diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 203ef53b27..3e137477d9 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -162,7 +162,7 @@ - The [`_Unwind_Context`](../../../core/libs/prx/libc/include/exceptions/Unwind.hpp) that `_Unwind_Backtrace` passes to its callback starts with a table laid out like LLVM libunwind's `AbstractUnwindCursor` virtual table, because homebrew built with the ps5-payload-dev SDK links libunwind statically and reads the frames with its own `_Unwind_GetIP` (`__unw_get_reg`), which calls through that table. Only register reads and writes (`validReg`, `getReg`, `setReg`, with `UNW_REG_IP` and `UNW_REG_SP`), `validFloatReg` (always false) and `isSignalFrame` are answered; the other slots throw. The layout follows libunwind's `UnwindCursor.hpp`, not the console's own `libSceLibcInternal`, which is not checked - The Windows libc [exception unwinder](../../../core/libs/prx/libc/src/exception/Unwind.cpp) cannot run arbitrary Windows exception or termination handlers. It accepts GCC C and C++ handlers identified by a project's `.sehgcc` section and interprets their LSDA with the guest personalities. A handler in an unmarked module, such as a C++ cleanup through `__gxx_personality_seh0` in `libstdc++-6.dll` or a system DLL function like `qsort` in `ucrtbase.dll` (Windows 10 22H2), still stops unwinding and the exception is reported as unhandled. SjLj exceptions are unsupported; Scoop GCC 15.2.0 selects them when SEH is disabled and lacks the System V ABI SEH compiler fix carried by MSYS2. - The libc [SSE4a trap emulation](../../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. -- [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists host interfaces and IPv4/IPv6 addresses in FreeBSD's layouts. Windows and macOS omit per-interface AF_LINK entries and interfaces without an address; on Windows the flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST` (PPP/tunnel adapters omit broadcast and point-to-point, and an unspecified prefix length gets a host mask). On Linux, `AF_LINK` `if_data` maps non-Ethernet/loopback/PPP types to `IFT_OTHER` (1) and leaves `ifi_omcasts`, `ifi_noproto`, `ifi_hwassist`, `ifi_metric`, `ifi_epoch`, `ifi_lastchange`, unknown baud rates and address-entry `ifa_data` as 0/null; a point-to-point address without a peer loses `IFF_POINTOPOINT`, and an omitted IPv4 broadcast is derived from the netmask. Interface names are the host's (`if` on Windows above 15 bytes). +- [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists host interfaces and IPv4/IPv6 addresses in FreeBSD's layouts. Windows omits per-interface AF_LINK entries and interfaces without an address; its flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST` (PPP/tunnel adapters omit broadcast and point-to-point, and an unspecified prefix length gets a host mask). On Linux, `AF_LINK` `if_data` maps non-Ethernet/loopback/PPP types to `IFT_OTHER` (1) and leaves `ifi_omcasts`, `ifi_noproto`, `ifi_hwassist`, `ifi_metric`, `ifi_epoch`, `ifi_lastchange`, unknown baud rates and address-entry `ifa_data` as 0/null; on macOS its 32-bit host counters wrap and `ifi_oqdrops`, `ifi_hwassist`, `ifi_epoch` and `ifi_lastchange` are 0; a point-to-point address without a peer loses `IFF_POINTOPOINT`, and an omitted IPv4 broadcast is derived from the netmask. Interface names are the host's (`if` on Windows above 15 bytes). - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports an empty name without querying the host machine name, as a payload process read on a PS5 (firmware 13.60): 0 with only the terminator for a 256-byte and a 1-byte buffer. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); whether a title process or a console with a configured name reads another name, and the integration with `sysctl` and name resolution, are unverified and not implemented. - [sinh](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`e_sinh.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_sinh.c) over this file's msun `Expm1` for |x| < 22, where it gives msun's bits (0 of 2,000,000 random arguments in [-20, 20] differ from a locally built msun on a Windows host). From 22 on it calls this file's `exp`, which is still the host's, so those results can differ from msun in the last bit until `exp` follows msun; the last step before the overflow threshold scales that `exp` as msun's `__ldexp_exp` does. On overflow it sets `errno` to `ERANGE`, as the host's `sinh` did and `sinhf` does; msun itself sets none. It was compared with msun built on the host, not with a console: the titles' `libc.prx` and the system `libSceLibcInternal` are unmeasured. - [wctomb_s, wcrtomb_s, mbstowcs_s and mbsrtowcs_s](../../../core/libs/prx/libc/src/BoundsChecking.cpp) (libc) follow the console's `libc.prx`, but convert in the single-byte C locale of `wctomb`, `wcrtomb` and `mbrtowc` (`MB_CUR_MAX` 1): a character above 0xFF is `EILSEQ` in `wctomb_s` and `wcrtomb_s`, and each byte is one wide character in `mbstowcs_s` and `mbsrtowcs_s`. The locale of a title's `libc.prx` was not measured. The `wcrtomb_s` path where a character does not fit `smax` and the `EILSEQ` paths of `mbstowcs_s` and `mbsrtowcs_s` are not reached From d702b9d7e545f56d206e3cf45fa7b42a392f0805 Mon Sep 17 00:00:00 2001 From: Nicolas Gonzalez Date: Tue, 27 Oct 2026 23:02:57 -0300 Subject: [PATCH 044/160] fix(libc): map macOS interface types to FreeBSD's as on Linux macOS numbers gif and stf 0x37 and 0x39, where FreeBSD uses 0xf0 and 0xd7, and has types FreeBSD lacks such as IFT_CELLULAR. The macOS AF_LINK type now maps like the Linux one: Ethernet, loopback and PPP keep their numbers and the others report IFT_OTHER. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Interfaces.cpp | 4 +++- core/libs/tests/GuestInterfaces.cpp | 3 ++- docs/dev/debt/libc.md | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libc/src/Interfaces.cpp b/core/libs/prx/libc/src/Interfaces.cpp index bbfee667a9..5dd301556e 100644 --- a/core/libs/prx/libc/src/Interfaces.cpp +++ b/core/libs/prx/libc/src/Interfaces.cpp @@ -15,6 +15,7 @@ #elif defined(__APPLE__) #include #include +#include #endif #include #include @@ -361,7 +362,8 @@ int HostEntries(std::vector& entries) { HostInterface& owner = interfaceFor(CheckedName(item->ifa_name)); if (owner.linked) throw std::runtime_error("getifaddrs: two link-level entries for " + owner.name); if (link.sdl_index == 0) throw std::runtime_error("getifaddrs: link-level entry of " + owner.name + " has no interface index"); - const std::uint8_t type = link.sdl_type; + const std::uint8_t type = link.sdl_type == IFT_ETHER ? guestTypeEthernet : link.sdl_type == IFT_LOOP ? guestTypeLoopback + : link.sdl_type == IFT_PPP ? guestTypePpp : guestTypeOther; const std::uint8_t hardwareLength = type == guestTypeLoopback ? 0 : link.sdl_alen; owner.link = Entry{owner.name, InterfaceFlags(item->ifa_flags), GuestLinkAddress(owner.name, link.sdl_index, type, reinterpret_cast(link.sdl_data + link.sdl_nlen), hardwareLength), {}, {}, diff --git a/core/libs/tests/GuestInterfaces.cpp b/core/libs/tests/GuestInterfaces.cpp index 47ee2a196b..94e0d0ffd8 100644 --- a/core/libs/tests/GuestInterfaces.cpp +++ b/core/libs/tests/GuestInterfaces.cpp @@ -14,6 +14,7 @@ #include #elif defined(__APPLE__) #include +#include #endif #include #include @@ -187,7 +188,7 @@ static std::vector HostSnapshot() { entry.family = AF_LINK; std::strncpy(entry.name, item->ifa_name, sizeof(entry.name) - 1); entry.index = link.sdl_index; - entry.type = link.sdl_type; + entry.type = link.sdl_type == IFT_ETHER ? 0x6 : link.sdl_type == IFT_LOOP ? 0x18 : link.sdl_type == IFT_PPP ? 0x17 : 0x1; entry.hardwareLength = entry.type == 0x18 ? 0 : std::min(link.sdl_alen, sizeof(entry.hardware)); std::memcpy(entry.hardware, link.sdl_data + link.sdl_nlen, entry.hardwareLength); entry.loopbackUp = (item->ifa_flags & IFF_UP) != 0; diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 3e137477d9..ca9efea837 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -162,7 +162,7 @@ - The [`_Unwind_Context`](../../../core/libs/prx/libc/include/exceptions/Unwind.hpp) that `_Unwind_Backtrace` passes to its callback starts with a table laid out like LLVM libunwind's `AbstractUnwindCursor` virtual table, because homebrew built with the ps5-payload-dev SDK links libunwind statically and reads the frames with its own `_Unwind_GetIP` (`__unw_get_reg`), which calls through that table. Only register reads and writes (`validReg`, `getReg`, `setReg`, with `UNW_REG_IP` and `UNW_REG_SP`), `validFloatReg` (always false) and `isSignalFrame` are answered; the other slots throw. The layout follows libunwind's `UnwindCursor.hpp`, not the console's own `libSceLibcInternal`, which is not checked - The Windows libc [exception unwinder](../../../core/libs/prx/libc/src/exception/Unwind.cpp) cannot run arbitrary Windows exception or termination handlers. It accepts GCC C and C++ handlers identified by a project's `.sehgcc` section and interprets their LSDA with the guest personalities. A handler in an unmarked module, such as a C++ cleanup through `__gxx_personality_seh0` in `libstdc++-6.dll` or a system DLL function like `qsort` in `ucrtbase.dll` (Windows 10 22H2), still stops unwinding and the exception is reported as unhandled. SjLj exceptions are unsupported; Scoop GCC 15.2.0 selects them when SEH is disabled and lacks the System V ABI SEH compiler fix carried by MSYS2. - The libc [SSE4a trap emulation](../../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. -- [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists host interfaces and IPv4/IPv6 addresses in FreeBSD's layouts. Windows omits per-interface AF_LINK entries and interfaces without an address; its flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST` (PPP/tunnel adapters omit broadcast and point-to-point, and an unspecified prefix length gets a host mask). On Linux, `AF_LINK` `if_data` maps non-Ethernet/loopback/PPP types to `IFT_OTHER` (1) and leaves `ifi_omcasts`, `ifi_noproto`, `ifi_hwassist`, `ifi_metric`, `ifi_epoch`, `ifi_lastchange`, unknown baud rates and address-entry `ifa_data` as 0/null; on macOS its 32-bit host counters wrap and `ifi_oqdrops`, `ifi_hwassist`, `ifi_epoch` and `ifi_lastchange` are 0; a point-to-point address without a peer loses `IFF_POINTOPOINT`, and an omitted IPv4 broadcast is derived from the netmask. Interface names are the host's (`if` on Windows above 15 bytes). +- [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists host interfaces and IPv4/IPv6 addresses in FreeBSD's layouts. Windows omits per-interface AF_LINK entries and interfaces without an address; its flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST` (PPP/tunnel adapters omit broadcast and point-to-point, and an unspecified prefix length gets a host mask). On Linux and macOS, `AF_LINK` maps non-Ethernet/loopback/PPP types to `IFT_OTHER` (1); Linux's `if_data` leaves `ifi_omcasts`, `ifi_noproto`, `ifi_hwassist`, `ifi_metric`, `ifi_epoch`, `ifi_lastchange`, unknown baud rates and address-entry `ifa_data` as 0/null, and macOS's 32-bit host counters wrap and `ifi_oqdrops`, `ifi_hwassist`, `ifi_epoch` and `ifi_lastchange` are 0; a point-to-point address without a peer loses `IFF_POINTOPOINT`, and an omitted IPv4 broadcast is derived from the netmask. Interface names are the host's (`if` on Windows above 15 bytes). - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports an empty name without querying the host machine name, as a payload process read on a PS5 (firmware 13.60): 0 with only the terminator for a 256-byte and a 1-byte buffer. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); whether a title process or a console with a configured name reads another name, and the integration with `sysctl` and name resolution, are unverified and not implemented. - [sinh](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`e_sinh.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_sinh.c) over this file's msun `Expm1` for |x| < 22, where it gives msun's bits (0 of 2,000,000 random arguments in [-20, 20] differ from a locally built msun on a Windows host). From 22 on it calls this file's `exp`, which is still the host's, so those results can differ from msun in the last bit until `exp` follows msun; the last step before the overflow threshold scales that `exp` as msun's `__ldexp_exp` does. On overflow it sets `errno` to `ERANGE`, as the host's `sinh` did and `sinhf` does; msun itself sets none. It was compared with msun built on the host, not with a console: the titles' `libc.prx` and the system `libSceLibcInternal` are unmeasured. - [wctomb_s, wcrtomb_s, mbstowcs_s and mbsrtowcs_s](../../../core/libs/prx/libc/src/BoundsChecking.cpp) (libc) follow the console's `libc.prx`, but convert in the single-byte C locale of `wctomb`, `wcrtomb` and `mbrtowc` (`MB_CUR_MAX` 1): a character above 0xFF is `EILSEQ` in `wctomb_s` and `wcrtomb_s`, and each byte is one wide character in `mbstowcs_s` and `mbsrtowcs_s`. The locale of a title's `libc.prx` was not measured. The `wcrtomb_s` path where a character does not fit `smax` and the `EILSEQ` paths of `mbstowcs_s` and `mbsrtowcs_s` are not reached From c3c947e9c1e721fb0b58b1905b961cfea7621dc7 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Tue, 27 Oct 2026 22:13:35 -0400 Subject: [PATCH 045/160] fix(shader): keep the half bits of a 16-bit float image store's D16 data Co-Authored-By: Claude Opus 5.5 --- .../execution/ImageStoreFloatRounding.cpp | 37 +++++++++++++------ .../SpirvBackend/src/SpirvImageEmitter.cpp | 2 +- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreFloatRounding.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreFloatRounding.cpp index bef64fd6b2..20bb6f2a18 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreFloatRounding.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreFloatRounding.cpp @@ -43,6 +43,10 @@ alignas(256) constexpr std::array StoreCode{ 0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0280, 0xf0201f08, 0x0001141e, 0x7e3e0281, 0xf0201508, 0x0001141e, 0xbf810000, }; +alignas(256) constexpr std::array StoreD16Code{ + 0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0280, 0xf0201f08, 0x8001141e, + 0x7e3e0281, 0xf0201508, 0x8001141e, 0xbf810000, +}; struct Format { const char* name; @@ -141,28 +145,35 @@ std::array TextureDescriptor(const void* data, const Format& f ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), (format.components == 2u ? TwoChannelSwizzle : IdentitySwizzle) | (Type2D << 28u), 0u, 0u, 0u, 0u}; } -void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Format& format) { +std::uint16_t Half(std::uint32_t tid, std::uint32_t component) { + return HalfTowardZero(Value(tid, component)); +} + +void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Format& format, bool d16) { for (std::uint32_t tid = 0; tid < Threads; ++tid) { - for (std::uint32_t component = 0; component < 4u; ++component) Input[tid * 4u + component] = Value(tid, component); + for (std::uint32_t component = 0; component < 4u; ++component) { + Input[tid * 4u + component] = d16 ? component < 2u ? static_cast(Half(tid, component * 2u)) | (static_cast(Half(tid, component * 2u + 1u)) << 16u) : 0u : Value(tid, component); + } } + const auto& storeCode = d16 ? StoreD16Code : StoreCode; std::fill(texels, texels + TexelBytes, 0x5au); std::vector userData(16, 0u); const auto buffer = BufferDescriptor(Input.data(), static_cast(Input.size() * sizeof(Input[0]))); const auto texture = TextureDescriptor(texels, format); std::copy(buffer.begin(), buffer.end(), userData.begin()); std::copy(texture.begin(), texture.end(), userData.begin() + 4); - const std::span code(StoreCode); - const std::array memory{{{reinterpret_cast(StoreCode.data()), std::as_bytes(code)}}}; + const std::span code(storeCode); + const std::array memory{{{reinterpret_cast(storeCode.data()), std::as_bytes(code)}}}; const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ - {ShaderStage::Compute, reinterpret_cast(StoreCode.data()), code, 0, {}}, + {ShaderStage::Compute, reinterpret_cast(storeCode.data()), code, 0, {}}, {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, device.Target(), {0, 0, 0, 128} }; request.useCache = false; const auto result = ShaderRecompiler::Recompile(request); - device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(StoreCode.data())); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(storeCode.data())); device.WaitIdle(); AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), TexelBytes, nullptr, "test"); device.WaitIdle(); @@ -172,10 +183,10 @@ std::uint16_t ExpectedComponent(std::uint32_t tid, std::uint32_t dmask, std::uin if (((dmask >> component) & 1u) == 0u) return 0u; std::uint32_t source = 0u; for (std::uint32_t lower = 0u; lower < component; ++lower) source += (dmask >> lower) & 1u; - return HalfTowardZero(Value(tid, source)); + return Half(tid, source); } -void Check(const std::uint8_t* texels, const Format& format) { +void Check(const std::uint8_t* texels, const Format& format, bool d16) { for (std::uint32_t y = 0; y < Height; ++y) { for (std::uint32_t x = 0; x < Width; ++x) { const auto offset = TexelOffset(format, x, y); @@ -184,7 +195,7 @@ void Check(const std::uint8_t* texels, const Format& format) { std::memcpy(&actual, texels + offset + component * sizeof(actual), sizeof(actual)); const auto dmask = y == Writes[0].row ? Writes[0].dmask : y == Writes[1].row ? Writes[1].dmask : 0u; const auto expected = dmask != 0u ? ExpectedComponent(x, dmask, component) : 0x5a5au; - Require(actual == expected, std::string("image_store of ") + format.name + ": texel (" + std::to_string(x) + ", " + std::to_string(y) + ") component " + std::to_string(component) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected)); + Require(actual == expected, std::string(d16 ? "D16 image_store of " : "image_store of ") + format.name + ": texel (" + std::to_string(x) + ", " + std::to_string(y) + ") component " + std::to_string(component) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected)); } } } @@ -197,9 +208,11 @@ int main() { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; GuestBlock block; - for (const auto& format : Formats) { - Store(*device, block.Data(), format); - Check(block.Data(), format); + for (const bool d16 : {false, true}) { + for (const auto& format : Formats) { + Store(*device, block.Data(), format, d16); + Check(block.Data(), format, d16); + } } std::puts("image store float rounding tests passed"); return 0; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 1e8a30d17e..051aa8a097 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -837,7 +837,7 @@ std::uint32_t StoreTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& acce raw = Select(state, TypeU32(state), Binary(state, spv::OpIEqual, TypeBool(state), RuntimeImageSwizzle(state, mem.resource, source), ConstantU32(state, component + 4u)), candidate, raw); } values[component] = integer ? raw : mem.dataBits == 16u ? EmitF16BitsToF32(state, raw) : Unary(state, spv::OpBitcast, TypeF32(state), raw); - if (float16 && component < storageFormat.componentCount) { + if (float16 && component < storageFormat.componentCount && mem.dataBits != 16u) { values[component] = EmitFormatStoreComponent(state, storageFormat, component, values[component]); } if (component < sint.componentCount) { From 994049e8bf8203b32e01e6ae299229b8c8f61abf Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Tue, 27 Oct 2026 22:51:33 -0500 Subject: [PATCH 046/160] test(libSceRtc): cover null pointer validation in current tick getters --- core/libs/tests/GuestRtc.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 57a213635e..d904988708 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -939,6 +939,11 @@ int main(int argc, char** argv) { Require(sceRtcGetCurrentAdNetworkTick(¤t) == 0 && current.tick >= adClock.tick && current.tick < adClock.tick + 1000000); Require(sceRtcGetCurrentDebugNetworkTick(¤t) == 0 && current.tick >= debugClock.tick && current.tick < debugClock.tick + 1000000); Require(sceRtcGetCurrentRawNetworkTick(¤t) == 0 && current.tick > unixEpochTick); + Require(sceRtcGetCurrentTick(nullptr) == invalidPointer); + Require(sceRtcGetCurrentNetworkTick(nullptr) == invalidPointer); + Require(sceRtcGetCurrentRawNetworkTick(nullptr) == invalidPointer); + Require(sceRtcGetCurrentAdNetworkTick(nullptr) == invalidPointer); + Require(sceRtcGetCurrentDebugNetworkTick(nullptr) == invalidPointer); } SetCompiledSdkVersion(process.data(), 0x02ffffff); Require(sceRtcGetDayOfWeek(0, 13, 1) == invalidYear && sceRtcGetDayOfWeek(10000, 1, 1) == 6); From 75294b0c5168214a99745ad04acb881a485b5ff3 Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Tue, 27 Oct 2026 22:43:21 -0500 Subject: [PATCH 047/160] test(libSceCesCs): cover null buffer checks in sceCesUtf16beToEucKr and sceCesUtf16leToUhc --- core/libs/tests/GuestCesDbcs.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/libs/tests/GuestCesDbcs.cpp b/core/libs/tests/GuestCesDbcs.cpp index bd3edaa068..dda2ed96af 100644 --- a/core/libs/tests/GuestCesDbcs.cpp +++ b/core/libs/tests/GuestCesDbcs.cpp @@ -402,6 +402,15 @@ int main() { Require(sceCesUtf16ToUhc(highSurrogate, 2, &read, uhc, out, 2, &size) == invalidEncode); Require(out[0] == 0x55 && out[1] == 0x55 && size == 0 && read == 77); + Require(sceCesUtf16beToEucKr(nullptr, 0, nullptr, uhc, nullptr, 0, nullptr) == invalidParameter); + Require(sceCesUtf16beToEucKr(nullptr, 0, nullptr, eucKr, nullptr, 0, nullptr) == invalidSrcBuffer); + Require(sceCesUtf16beToEucKr(&unit, 0, nullptr, eucKr, nullptr, 0, nullptr) == srcBufferEnd); + Require(sceCesUtf16beToEucKr(&unit, 1, nullptr, eucKr, nullptr, 0, nullptr) == invalidDstBuffer); + Require(sceCesUtf16leToUhc(nullptr, 0, nullptr, eucKr, nullptr, 0, nullptr) == invalidParameter); + Require(sceCesUtf16leToUhc(nullptr, 0, nullptr, uhc, nullptr, 0, nullptr) == invalidSrcBuffer); + Require(sceCesUtf16leToUhc(&unit, 0, nullptr, uhc, nullptr, 0, nullptr) == srcBufferEnd); + Require(sceCesUtf16leToUhc(&unit, 1, nullptr, uhc, nullptr, 0, nullptr) == invalidDstBuffer); + std::memcpy(&unit, "\xAC\x00", 2); Require(sceCesUtf16beToEucKr(&unit, 1, &read, eucKr, out, 2, &size) == 0 && read == 1 && size == 2 && std::memcmp(out, "\xB0\xA1", 2) == 0); std::memcpy(&unit, "\x00\xAC", 2); From 3f3b3c32406900da63ecedd05dddd76185796970 Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Tue, 27 Oct 2026 22:41:56 -0500 Subject: [PATCH 048/160] test(libSceCesCs): cover null buffer checks in sceCesUtf8ToEucCn and sceCesUtf8ToGb --- core/libs/tests/GuestCesChinese.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/tests/GuestCesChinese.cpp b/core/libs/tests/GuestCesChinese.cpp index d5c3ba5365..24e18b4458 100644 --- a/core/libs/tests/GuestCesChinese.cpp +++ b/core/libs/tests/GuestCesChinese.cpp @@ -358,6 +358,14 @@ int main() { Require(sceCesUcsProfileInitGb18030_2000(nullptr) == nullptr && sceCesUcsProfileInitGb18030_2005(nullptr) == nullptr); Require(sceCesEucCnToUcs2(gb2000, Bytes("\xD6\xD0"), 2, &consumed, &ucs2) == invalidParameter); Require(sceCesGbToUcs2(gb2005, Bytes("\xD6\xD0"), 2, &consumed, &ucs2) == 0 && consumed == 2 && ucs2 == 0x4E2D); + Require(sceCesUtf8ToEucCn(nullptr, 0, nullptr, gbk, nullptr, 0, nullptr) == invalidParameter); + Require(sceCesUtf8ToEucCn(nullptr, 0, nullptr, euc, nullptr, 0, nullptr) == invalidSrcBuffer); + Require(sceCesUtf8ToEucCn(Bytes("\xD6\xD0"), 0, nullptr, euc, nullptr, 0, nullptr) == srcBufferEnd); + Require(sceCesUtf8ToEucCn(Bytes("\xD6\xD0"), 2, nullptr, euc, nullptr, 0, nullptr) == invalidDstBuffer); + Require(sceCesUtf8ToGb(nullptr, 0, nullptr, euc, nullptr, 0, nullptr) == invalidParameter); + Require(sceCesUtf8ToGb(nullptr, 0, nullptr, gb2005, nullptr, 0, nullptr) == invalidSrcBuffer); + Require(sceCesUtf8ToGb(Bytes("\xD6\xD0"), 0, nullptr, gb2005, nullptr, 0, nullptr) == srcBufferEnd); + Require(sceCesUtf8ToGb(Bytes("\xD6\xD0"), 2, nullptr, gb2005, nullptr, 0, nullptr) == invalidDstBuffer); CheckGb18030(gb2000, false, 0xE61D3BD3, 0x83476043, 0x469CFCE5, 0, 0); CheckGb18030(gb2005, true, 0xE4CF78F0, 0x8D188DFC, 0xD675EFA1, 0, 0); return 0; From 2295183c6ccdee60a184630ad28d4c347adb7bb9 Mon Sep 17 00:00:00 2001 From: Michael Krumland-Dunning Date: Tue, 27 Oct 2026 21:36:00 -0700 Subject: [PATCH 049/160] fix(shader): translate v_div_scale_f32 and v_div_fixup_f32 in f32 denormal modes 1 and 2 Both threw "f32 denormal mode N is not implemented" whenever FLOAT_MODE kept only one side of the f32 denormals, so any LLVM-compiled f32 division in such a shader failed to translate. On RDNA2 (gfx1036) at round to nearest even, v_div_fixup_f32 follows bit 0 of the field, and v_div_scale_f32 flushes its inputs in mode 2 as in mode 0 and in mode 1 keeps them and flushes a denormal result, with the VCC of mode 3. The translation now matches all 2,304 measured rows in modes 1 and 2 at IEEE 0 and 1. Directed rounding still throws, as an underflowing v_div_fixup_f32 result rounds to the smallest denormal there. Division and F32DenormalFlush no longer require modes 1 and 2 to be refused; Division checks v_div_scale_f32 in them against its measured vectors instead. Co-Authored-By: Claude Opus 5.5 --- .../DivScaleFixupF32DenormalModes.cpp | 222 ++++++++++++++++++ .../tests/execution/Division.cpp | 20 +- .../tests/execution/F32DenormalFlush.cpp | 10 - .../Translation/src/VectorInstructions.cpp | 13 + docs/dev/debt/shader.md | 3 +- 5 files changed, 246 insertions(+), 22 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DivScaleFixupF32DenormalModes.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DivScaleFixupF32DenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DivScaleFixupF32DenormalModes.cpp new file mode 100644 index 0000000000..9709533a9b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DivScaleFixupF32DenormalModes.cpp @@ -0,0 +1,222 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 5; +constexpr std::uint32_t Stride = 8; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0x34040085u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xd56d6a0au, 0x04160904u, 0xd501000bu, + 0x01a90280u, 0xd56d6a0cu, 0x04160905u, 0xd501000du, 0x01a90280u, 0xd55f000eu, 0x04160906u, 0xe0781000u, + 0x80010a02u, 0xe0701010u, 0x80010e02u, 0xbf810000u, +}; + +constexpr const char* Names[Results] = { + "v_div_scale_f32 v10, vcc_lo, v4, v4, v5", + "its vcc_lo", + "v_div_scale_f32 v12, vcc_lo, v5, v4, v5", + "its vcc_lo", + "v_div_fixup_f32 v14, v6, v4, v5", +}; + +constexpr std::uint32_t Rows[Threads][3] = { + {0x7e800000u, 0x00000001u, 0x00000005u}, + {0x7f7fffffu, 0x807fffffu, 0x3f800000u}, + {0x7e800000u, 0x00400000u, 0x80400000u}, + {0xff7fffffu, 0x00000003u, 0x7fc00001u}, + {0x00000001u, 0x3f800000u, 0x00000005u}, + {0x007fffffu, 0x40490fdbu, 0x3f800000u}, + {0x80000001u, 0xbfc00000u, 0x80400000u}, + {0x00400000u, 0x7f7fffffu, 0x7fc00001u}, + {0x3f800000u, 0x00000003u, 0x00000005u}, + {0xbfc00000u, 0x80000001u, 0x3f800000u}, + {0x40490fdbu, 0x007fffffu, 0x80400000u}, + {0x5f800000u, 0x00400000u, 0x7fc00001u}, + {0x00000001u, 0x00000001u, 0x00000005u}, + {0x007fffffu, 0x807fffffu, 0x3f800000u}, + {0x00400000u, 0x00000003u, 0x80400000u}, + {0x00800000u, 0x00000001u, 0x7fc00001u}, + {0x00000001u, 0x00800000u, 0x00000005u}, + {0x0c000000u, 0x00000001u, 0x3f800000u}, + {0x00000003u, 0x0c800000u, 0x80400000u}, + {0x34000000u, 0x80000001u, 0x7fc00001u}, + {0x00000001u, 0x00000000u, 0x00000005u}, + {0x00000000u, 0x00000001u, 0x3f800000u}, + {0x7f800000u, 0x00000001u, 0x80400000u}, + {0x00000001u, 0xff800000u, 0x7fc00001u}, + {0x7fc00000u, 0x00000001u, 0x00000005u}, + {0x00000003u, 0x7fc00000u, 0x3f800000u}, + {0x00000001u, 0x7e800000u, 0x80400000u}, + {0x807fffffu, 0x5f800000u, 0x7fc00001u}, + {0x3f800000u, 0x3f800000u, 0x00000005u}, + {0x40490fdbu, 0xbfc00000u, 0x3f800000u}, + {0x0c800000u, 0x7f7fffffu, 0x80400000u}, + {0x00800000u, 0x0c000000u, 0x7fc00001u} +}; +constexpr std::uint32_t Mode1Expected[Threads][Results] = { + {0x5e800000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u}, + {0x5f7fffffu, 0x00000001u, 0x80000000u, 0x00000001u, 0x80000000u}, + {0x5e800000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u}, + {0xdf7fffffu, 0x00000001u, 0x00000000u, 0x00000001u, 0x80000000u}, + {0x15000000u, 0x00000001u, 0x3f800000u, 0x00000001u, 0x00000005u}, + {0x207ffffeu, 0x00000001u, 0x40490fdbu, 0x00000001u, 0x3f800000u}, + {0x95000000u, 0x00000001u, 0xbfc00000u, 0x00000001u, 0x00400000u}, + {0x20000000u, 0x00000001u, 0x7f7fffffu, 0x00000001u, 0x7f800000u}, + {0x3f800000u, 0x00000001u, 0x15c00000u, 0x00000001u, 0x00000005u}, + {0xbfc00000u, 0x00000001u, 0x95000000u, 0x00000001u, 0x3f800000u}, + {0x40490fdbu, 0x00000001u, 0x207ffffeu, 0x00000001u, 0x00400000u}, + {0x5f800000u, 0x00000001u, 0x20000000u, 0x00000001u, 0x00000000u}, + {0x15000000u, 0x00000000u, 0x15000000u, 0x00000000u, 0x00000005u}, + {0x207ffffeu, 0x00000000u, 0xa07ffffeu, 0x00000000u, 0xbf800000u}, + {0x20000000u, 0x00000000u, 0x15c00000u, 0x00000000u, 0x00400000u}, + {0x20800000u, 0x00000000u, 0x15000000u, 0x00000000u, 0x7f800000u}, + {0x15000000u, 0x00000000u, 0x20800000u, 0x00000000u, 0x00000005u}, + {0x2c000000u, 0x00000000u, 0x15000000u, 0x00000000u, 0x3f800000u}, + {0x15c00000u, 0x00000000u, 0x2c800000u, 0x00000000u, 0x00400000u}, + {0x34000000u, 0x00000001u, 0x95000000u, 0x00000001u, 0xff800000u}, + {0xffc00000u, 0x00000000u, 0xffc00000u, 0x00000000u, 0x00000000u}, + {0xffc00000u, 0x00000000u, 0xffc00000u, 0x00000000u, 0x7f800000u}, + {0x7f800000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x00000000u}, + {0x15000000u, 0x00000001u, 0xff800000u, 0x00000001u, 0xff800000u}, + {0x7fc00000u, 0x00000001u, 0x00000000u, 0x00000001u, 0x7fc00000u}, + {0x15c00000u, 0x00000001u, 0x7fc00000u, 0x00000001u, 0x7fc00000u}, + {0x15000000u, 0x00000001u, 0x7e800000u, 0x00000001u, 0x00400000u}, + {0xa07ffffeu, 0x00000001u, 0x5f800000u, 0x00000001u, 0xff800000u}, + {0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000005u}, + {0x40490fdbu, 0x00000000u, 0xbfc00000u, 0x00000000u, 0xbf800000u}, + {0x2c800000u, 0x00000001u, 0x7f7fffffu, 0x00000001u, 0x00400000u}, + {0x20800000u, 0x00000000u, 0x2c000000u, 0x00000000u, 0x7f800000u} +}; +constexpr std::uint32_t Mode2Expected[Threads][Results] = { + {0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x00000000u}, + {0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x80000000u}, + {0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x00000000u}, + {0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x80000000u}, + {0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x7f800000u}, + {0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x7f800000u}, + {0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x7f800000u}, + {0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x7f800000u}, + {0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x00000000u}, + {0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x00000000u}, + {0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x00000000u}, + {0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x00000000u}, + {0xffc00000u, 0x00000000u, 0xffc00000u, 0x00000000u, 0xffc00000u}, + {0xffc00000u, 0x00000000u, 0xffc00000u, 0x00000000u, 0xffc00000u}, + {0xffc00000u, 0x00000000u, 0xffc00000u, 0x00000000u, 0xffc00000u}, + {0xffc00000u, 0x00000000u, 0xffc00000u, 0x00000000u, 0x00000000u}, + {0xffc00000u, 0x00000000u, 0xffc00000u, 0x00000000u, 0x7f800000u}, + {0xffc00000u, 0x00000000u, 0xffc00000u, 0x00000000u, 0x00000000u}, + {0xffc00000u, 0x00000000u, 0xffc00000u, 0x00000000u, 0x7f800000u}, + {0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x80000000u}, + {0xffc00000u, 0x00000000u, 0xffc00000u, 0x00000000u, 0xffc00000u}, + {0xffc00000u, 0x00000000u, 0xffc00000u, 0x00000000u, 0xffc00000u}, + {0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x00000000u}, + {0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0xff800000u}, + {0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x7fc00000u}, + {0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x7fc00000u}, + {0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0x7f800000u}, + {0xffc00000u, 0x00000001u, 0xffc00000u, 0x00000001u, 0xff800000u}, + {0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000005u}, + {0x40490fdbu, 0x00000000u, 0xbfc00000u, 0x00000000u, 0xbf800000u}, + {0x2c800000u, 0x00000001u, 0x7f7fffffu, 0x00000001u, 0x00400000u}, + {0x20800000u, 0x00000000u, 0x2c000000u, 0x00000000u, 0x7f800000u} +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::optional Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode floatMode) { + Input.fill(0u); + for (std::uint32_t tid = 0; tid < Threads; ++tid) std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), &Input[tid * Inputs]); + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = floatMode; + std::optional result; + try { + result.emplace(ShaderRecompiler::Recompile(request)); + } catch (const std::exception& error) { + return std::string(error.what()); + } + device.Dispatch(*result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + return std::nullopt; +} + +void Check(std::uint32_t denormals, bool ieee) { + const auto& expected = denormals == 1u ? Mode1Expected : Mode2Expected; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t i = 0; i < Results; ++i) { + const std::uint32_t actual = Output[tid * Stride + i]; + Require(actual == expected[tid][i], "f32 denormal mode " + std::to_string(denormals) + (ieee ? ", IEEE=1, " : ", IEEE=0, ") + Names[i] + " with v4 " + Hex(Rows[tid][0]) + ", v5 " + Hex(Rows[tid][1]) + ", v6 " + Hex(Rows[tid][2]) + " is " + Hex(actual) + ", expected " + Hex(expected[tid][i])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const bool keepsDenormals = device->Target().float32DenormPreserve; + std::uint32_t checked = 0u; + for (const std::uint32_t denormals : {1u, 2u}) { + for (const bool ieee : {false, true}) { + const ShaderRecompiler::ShaderFloatMode floatMode{0xc0u | denormals << 4u, true, ieee, false}; + const auto refusal = Run(*device, floatMode); + if (refusal) { + Require(denormals == 1u && !keepsDenormals && refusal->find("needs DenormPreserve 32") != std::string::npos, "FLOAT_MODE " + Hex(floatMode.floatMode) + ": " + *refusal); + continue; + } + ++checked; + Check(denormals, ieee); + } + } + std::printf("v_div_scale_f32 and v_div_fixup_f32 denormal mode 1 and 2 tests passed (%u of 4 modes run; the others need DenormPreserve 32)\n", checked); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Division.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Division.cpp index 9cbfb1997d..1287be8ac8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Division.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Division.cpp @@ -700,14 +700,20 @@ bool HostFusesFma(AgcDriver::VulkanDevice& device) { } void CheckScaleDenormals(AgcDriver::VulkanDevice& device) { - const std::array, 5> modes{{ + const std::array, 9> modes{{ std::nullopt, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}, + ShaderRecompiler::ShaderFloatMode{0xd0u, true, false, false}, + ShaderRecompiler::ShaderFloatMode{0xd0u, true, true, false}, + ShaderRecompiler::ShaderFloatMode{0xe0u, true, false, false}, + ShaderRecompiler::ShaderFloatMode{0xe0u, true, true, false}, ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}, ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, }}; for (const auto& mode : modes) { + const std::uint32_t denormals = mode.has_value() ? (mode->floatMode >> 4u) & 3u : 0u; + if (denormals == 1u && !device.Target().float32DenormPreserve) continue; Shader shader(device, ScaleCode, mode); Input.fill(0u); for (std::uint32_t lane = 0; lane < std::size(ScaleVectors); ++lane) { @@ -718,20 +724,12 @@ void CheckScaleDenormals(AgcDriver::VulkanDevice& device) { for (std::uint32_t lane = 0; lane < std::size(ScaleVectors); ++lane) { const auto& vector = ScaleVectors[lane]; const auto* out = &Output[lane * Stride]; - const auto expected = mode.has_value() && ((mode->floatMode >> 4u) & 3u) == 3u ? vector.kept : vector.flushed; + const std::uint32_t keptResult = (vector.kept & 0x7f800000u) == 0u ? vector.kept & 0x80000000u : vector.kept; + const auto expected = denormals == 3u ? vector.kept : denormals == 1u ? keptResult : vector.flushed; const std::string where = "division scale denormals: lane " + std::to_string(lane) + " mode " + (mode.has_value() ? Hex(mode->floatMode) : "unknown"); Require(out[0] == expected && out[1] == vector.vcc, where + ": v_div_scale_f32 is " + Hex(out[0]) + " vcc " + std::to_string(out[1]) + ", expected " + Hex(expected) + " vcc " + std::to_string(vector.vcc)); } } - for (const std::uint32_t mode : {0xd0u, 0xe0u}) { - bool refused = false; - try { - Shader shader(device, ScaleCode, ShaderRecompiler::ShaderFloatMode{mode, true, false, false}); - } catch (const std::runtime_error& error) { - refused = std::string(error.what()).find("f32 denormal mode") != std::string::npos; - } - Require(refused, "division scale denormals: FLOAT_MODE " + Hex(mode) + " was not refused"); - } } void CheckHelpers(AgcDriver::VulkanDevice& device, bool fused) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp index bb4aa369e0..24a9a308cc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include @@ -555,15 +554,6 @@ int main() { CheckTinyLegacy("round mode " + std::to_string(round)); CheckTinyDirected(round); } - for (const std::uint32_t mode : {0xd0u, 0xe0u}) { - bool refused = false; - try { - Run(*device, ShaderRecompiler::ShaderFloatMode{mode, true, false, false}); - } catch (const std::runtime_error& error) { - refused = std::string(error.what()).find("f32 denormal mode") != std::string::npos; - } - Require(refused, "f32 denormal flush: FLOAT_MODE " + Hex(mode) + " was not refused"); - } } std::puts("f32 denormal flush tests passed"); return 0; diff --git a/core/shader/recompiler/Translation/src/VectorInstructions.cpp b/core/shader/recompiler/Translation/src/VectorInstructions.cpp index 20a45cee03..02f81e4472 100644 --- a/core/shader/recompiler/Translation/src/VectorInstructions.cpp +++ b/core/shader/recompiler/Translation/src/VectorInstructions.cpp @@ -125,6 +125,19 @@ std::uint32_t TranslationContext::f32DenormalFlushFor(const RdnaInstruction& ins return floatMode.has_value() && ((floatMode->floatMode >> 4u) & 1u) != 0u ? 0u : 1u; case RdnaOpcode::VDivScaleF32: case RdnaOpcode::VDivFixupF32: + if (floatMode.has_value() && (floatMode->floatMode & 3u) == 0u) { + const std::uint32_t denormals = (floatMode->floatMode >> 4u) & 3u; + if (denormals == 2u) return 1u; + if (denormals == 1u && inst.op == RdnaOpcode::VDivFixupF32) return 0u; + if (denormals == 1u) { + if (!float32DenormPreserve || float32FlushToZero) { + throw std::runtime_error("f32 denormal mode 1 at pc " + std::to_string(inst.programCounter) + " needs DenormPreserve 32, which the device does not offer for this shader"); + } + return 2u; + } + } + flush = 1u; + break; case RdnaOpcode::VMadMixloF16: case RdnaOpcode::VMadMixhiF16: flush = 1u; diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 9bd5810262..713ad01505 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -141,7 +141,8 @@ - [Ordered not-equal, less-than and greater-than compares](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual`, and `FPOrdLessThan32` and `FPOrdGreaterThan32` as their ordered forms, each with both operands checked for NaN on their bits; the results are the same for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of not-equal feeds a ballot on a 32-wide subgroup (#825), and so does a less-than or greater-than against an infinity the driver sees as a constant (#3536), presumably rewritten into a not-equal by its compiler - [16-bit integer multiplies](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`v_mul_lo_u16`, `v_pk_mul_lo_u16`) (shader recompiler) - a factor taken from the low half of its source is read as the low 24 bits of the register, not the low 16; the low 16 bits of the product are the same for every input. The AMD Windows driver 32.0.21045.5002 (RX 6700 XT, RDNA 2) stores the full 32-bit product when two values it knows fit in 16 bits are multiplied, the product is reduced to 16 bits and nothing else is merged into the stored value: `v_mul_lo_u16` into a register whose high half is zero, also with factors masked to 16 bits by the program, and `v_pk_mul_lo_u16` whose high lanes are zero wrote 0xfffe0001 for 0xffff * 0xffff where the GPU writes 0x00000001. Reducing the product with `v_and_b32`, `v_bfe_u32` or a shift pair loses the same bits; factors read as 24-bit fields do not. The [constant folder](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) keeps a field read that a multiply uses, even when the program already masked the factor to 16 bits, since folding it away brings the full product back (#8271). Measured against the same instructions run through the [hardware oracle](../HW_ORACLE.md) on that GPU; other drivers are not checked - [f32 compares in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) (`v_cmp_X_f32` and `v_cmpx_X_f32` with X = eq, ge, gt, le, lg, lt, neq, nge, ngt, nle, nlg, nlt) flush their f32 denormal inputs by bit 0 of the `FLOAT_MODE` f32 denormal field: mode 1 keeps them as mode 3 does, and mode 2 flushes them as mode 0 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 484 operand pairs of zeros, denormals, minimum normals, ones, the largest finite value, infinities and quiet and signaling NaNs of both signs, in VOPC form, at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3, f32 round to nearest even, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0. The PS5's gfx1013 is not measured. -- [f32 conversions in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) (`v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_pknorm_i16/u16_f32`, `v_cvt_pk_u8_f32`, `v_cvt_pkrtz_f16_f32` and `v_cvt_f16_f32`) flush their f32 denormal inputs by bit 0 of the `FLOAT_MODE` f32 denormal field: mode 1 keeps them as mode 3 does, and mode 2 flushes them as mode 0 does. Measured on RDNA2 (gfx1036, ROCm 7.1.1, Linux 7.2) over 148 sources and 484 operand pairs (zeros, denormals, minimum normals, f16 limits, the largest finite value, infinities, NaNs) at IEEE=0 and IEEE=1 in every f32 denormal and round mode, and for the f16 and f64 results in every f16/f64 denormal and round mode. `v_div_scale_f32` and `v_div_fixup_f32` do not follow this rule and still throw in modes 1 and 2, as do `v_mad_mixlo/mixhi_f16`, which RDNA2 lacks. The PS5's gfx1013 is not measured. +- [f32 conversions in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) (`v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_pknorm_i16/u16_f32`, `v_cvt_pk_u8_f32`, `v_cvt_pkrtz_f16_f32` and `v_cvt_f16_f32`) flush their f32 denormal inputs by bit 0 of the `FLOAT_MODE` f32 denormal field: mode 1 keeps them as mode 3 does, and mode 2 flushes them as mode 0 does. Measured on RDNA2 (gfx1036, ROCm 7.1.1, Linux 7.2) over 148 sources and 484 operand pairs (zeros, denormals, minimum normals, f16 limits, the largest finite value, infinities, NaNs) at IEEE=0 and IEEE=1 in every f32 denormal and round mode, and for the f16 and f64 results in every f16/f64 denormal and round mode. `v_mad_mixlo/mixhi_f16`, which RDNA2 lacks, still throw in modes 1 and 2. The PS5's gfx1013 is not measured. +- [v_div_scale_f32 and v_div_fixup_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) throw with a directed f32 round mode, where an underflowing `v_div_fixup_f32` result rounds to the smallest denormal and bit 1 of the `FLOAT_MODE` f32 denormal field flushes it, and in mode 1 `v_div_scale_f32` throws when the module cannot declare `DenormPreserve 32`; the PS5's gfx1013 is not measured. At round to nearest even `v_div_fixup_f32` follows bit 0 (mode 1 as mode 3, mode 2 as mode 0), and `v_div_scale_f32` flushes its inputs in mode 2 as in mode 0 and in mode 1 keeps them and flushes a denormal result to a zero of its sign, with the VCC of mode 3. Measured on RDNA2 (gfx1036, AMD Windows driver 32.0.21030.31 through HIP) over 2,304 rows (24 x 24 operand pairs with src0 the denominator and the numerator, four quotients) at IEEE 0 and 1 in every f32 denormal and round mode. - [f16 compares](../../../core/shader/recompiler/Translation/src/CompareInstructions.cpp) (`v_cmp_X_f16` and `v_cmpx_X_f16` with X = eq, ge, gt, le, lg, lt, neq, nge, ngt, nle, nlg, nlt) flush their f16 denormal inputs to a zero of the same sign by bit 0 of the `FLOAT_MODE` f16 denormal field: modes 0 and 2 flush them, 1 and 3 keep them. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) over 256 operand pairs of zeros, denormals, minimum normals, ones, the largest finite value, infinities and quiet and signaling NaNs of both signs, in VOPC form, at IEEE=0 and IEEE=1 with each f16 denormal mode 0 to 3, f16 round to nearest even, DX10_CLAMP=1, f32 denormals kept and FP16_OVFL=0. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [DB_SHADER_CONTROL.PRIMITIVE_ORDERED_PIXEL_SHADER](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the host pixel interlock spans the whole shader, not only up to `s_sendmsg` ORDERED_PS_DONE. - [POPS collision SGPRs](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the collision wave ID and intrawave collision mask read 0, so a title skips its POPS wait and runs its per-collision loop once. From 46f658e8dd9748c0912b06259012b251c83b3a29 Mon Sep 17 00:00:00 2001 From: Matheus Herberts <163153928+Mherberts@users.noreply.github.com> Date: Wed, 28 Oct 2026 02:23:06 -0300 Subject: [PATCH 050/160] feat(relinker): lower MULX under --to-rosetta --- core/relinker/CMakeLists.txt | 11 + .../include/codegen/x86/MulxLowering.hpp | 22 ++ .../src/x86/Amd64OnlyInstructionMatcher.cpp | 7 + .../relinker/codegen/src/x86/MulxLowering.cpp | 117 ++++++ .../codegen/tests/MulxLoweringTests.cpp | 356 ++++++++++++++++++ .../relinker/tests/test_mulx_conversion.py | 51 +++ docs/dev/debt/relinker.md | 2 + 7 files changed, 566 insertions(+) create mode 100644 core/relinker/codegen/include/codegen/x86/MulxLowering.hpp create mode 100644 core/relinker/codegen/src/x86/MulxLowering.cpp create mode 100644 core/relinker/codegen/tests/MulxLoweringTests.cpp create mode 100644 core/relinker/relinker/tests/test_mulx_conversion.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index a166162988..823490aacc 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -34,6 +34,7 @@ add_library(relinker_core STATIC ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileIdentity.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/Amd64OnlyConverter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/RedZoneFramer.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/MulxLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionRewriter.cpp @@ -106,6 +107,11 @@ if(BUILD_TESTING) add_test(NAME masked_store_lowering COMMAND masked_store_lowering_tests) set_tests_properties(masked_store_lowering PROPERTIES TIMEOUT 20) + add_test_executable(mulx_lowering_tests ${CMAKE_CURRENT_SOURCE_DIR}/codegen/tests/MulxLoweringTests.cpp) + target_link_libraries(mulx_lowering_tests PRIVATE relinker_core) + add_test(NAME mulx_lowering COMMAND mulx_lowering_tests) + set_tests_properties(mulx_lowering PROPERTIES TIMEOUT 20) + add_test_executable(relinker_conversion_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/ConversionTests.cpp) target_link_libraries(relinker_conversion_tests PRIVATE relinker_core) add_test(NAME relinker_conversion COMMAND relinker_conversion_tests) @@ -211,6 +217,7 @@ if(WIN32) ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsPeWriter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/BufferUtils.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/Amd64OnlyConverter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/MulxLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionRewriter.cpp @@ -483,6 +490,7 @@ if(BUILD_TESTING) add_test_executable(amd64_only_converter_tests ${CMAKE_CURRENT_SOURCE_DIR}/codegen/tests/Amd64OnlyConverterTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/Amd64OnlyConverter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/MulxLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionRewriter.cpp @@ -895,6 +903,9 @@ if(BUILD_TESTING) add_test(NAME guest_gnu_hash COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_gnu_hash.py $) set_tests_properties(guest_gnu_hash PROPERTIES TIMEOUT 20) + add_test(NAME mulx_conversion COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_mulx_conversion.py $) + set_tests_properties(mulx_conversion PROPERTIES TIMEOUT 20) add_test(NAME to_rosetta COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_to_rosetta.py $) add_test(NAME syscall_number COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/codegen/include/codegen/x86/MulxLowering.hpp b/core/relinker/codegen/include/codegen/x86/MulxLowering.hpp new file mode 100644 index 0000000000..5a27b798f2 --- /dev/null +++ b/core/relinker/codegen/include/codegen/x86/MulxLowering.hpp @@ -0,0 +1,22 @@ +#ifndef CODEGEN_X86_MULXLOWERING_HPP +#define CODEGEN_X86_MULXLOWERING_HPP + +#include +#include + +namespace Codegen { + +struct MulxOperands { + std::uint8_t HighDestination; + std::uint8_t LowDestination; + std::uint8_t Source; + bool Wide; + std::optional Memory; +}; + +[[nodiscard]] std::optional DecodeMulx(const std::uint8_t* data, std::size_t length); +void EmitMulx(StubBodyBuilder& body, const MulxOperands& operands); + +} + +#endif diff --git a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp index ecbe487147..f957478801 100644 --- a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp +++ b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -277,6 +278,8 @@ std::optional Amd64OnlyInstructionMatcher::_matchRosetta(const D return _trampoline(kScalarCompare.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { _emitScalarCompare(body, instr, *compare); }), true); if (const auto masked = DecodeMaskedStore(instr.Data, instr.Length)) return _trampoline(masked->Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { EmitMaskedStore(body, *masked); })); + if (const auto mulx = DecodeMulx(instr.Data, instr.Length)) + return _trampoline("MULX", instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { EmitMulx(body, *mulx); })); return std::nullopt; } @@ -340,6 +343,10 @@ const char* Amd64OnlyInstructionMatcher::_emitOutOfLine(StubBodyBuilder& body, c EmitMaskedStore(body, *masked); return masked->Name; } + if (const auto mulx = _target == Amd64OnlyTarget::Rosetta ? DecodeMulx(instr.Data, instr.Length) : std::nullopt) { + EmitMulx(body, *mulx); + return "MULX"; + } return nullptr; } diff --git a/core/relinker/codegen/src/x86/MulxLowering.cpp b/core/relinker/codegen/src/x86/MulxLowering.cpp new file mode 100644 index 0000000000..174354d950 --- /dev/null +++ b/core/relinker/codegen/src/x86/MulxLowering.cpp @@ -0,0 +1,117 @@ +#include +#include +#include +#include +#include + +namespace Codegen { + +namespace { + +constexpr std::int32_t kFrameSize = 192; +constexpr std::uint8_t kStack = 4; + +void _append32(std::vector& bytes, const std::uint32_t value) { + for (unsigned shift = 0; shift < 32; shift += 8) + bytes.push_back(static_cast(value >> shift)); +} + +std::uint8_t _rex(const bool wide, const std::uint8_t reg, const std::uint8_t rm) { + return static_cast(0x40 | (wide ? 8 : 0) | ((reg & 8) >> 1) | ((rm & 8) >> 3)); +} + +void _stack(StubBodyBuilder& body, const std::uint8_t opcode, const bool wide, const std::uint8_t reg, const std::int32_t displacement) { + std::vector bytes{_rex(wide, reg, kStack), opcode, static_cast(0x84 | ((reg & 7) << 3)), 0x24}; + _append32(bytes, static_cast(displacement)); + body.Raw(bytes); +} + +void _loadSource(StubBodyBuilder& body, const MulxOperands& operands) { + if (!operands.Memory) { + if (operands.Source == kStack) { + _stack(body, 0x8D, operands.Wide, 0, kFrameSize); + } else { + const std::array bytes{_rex(operands.Wide, 0, operands.Source), std::uint8_t{0x8B}, static_cast(0xC0 | (operands.Source & 7))}; + body.Raw(bytes); + } + return; + } + const auto& memory = *operands.Memory; + auto displacement = static_cast(memory.Displacement); + if (memory.StackBase) displacement += kFrameSize; + const bool address32 = std::find(memory.Prefixes.begin(), memory.Prefixes.end(), 0x67) != memory.Prefixes.end(); + if (!address32 && displacement > std::numeric_limits::max()) + throw CodegenException("MULX stack displacement does not fit after spilling"); + const auto mod = memory.StackBase ? 2 : memory.Mod; + auto bytes = memory.Prefixes; + bytes.push_back(static_cast(_rex(operands.Wide, 0, 0) | memory.RexIndexBase)); + bytes.push_back(0x8B); + bytes.push_back(static_cast((mod << 6) | memory.Rm)); + if (memory.Rm == 4) bytes.push_back(memory.Sib); + const auto offset = bytes.size(); + if (mod == 1) bytes.push_back(static_cast(displacement)); + if (mod == 2 || (mod == 0 && (memory.RipRelative || (memory.Rm == 4 && (memory.Sib & 7) == 5)))) + _append32(bytes, static_cast(displacement)); + if (memory.RipRelative) + body.RipRelative(bytes, offset, static_cast(memory.InstructionLength) + memory.Displacement); + else + body.Raw(bytes); +} + +} + +std::optional DecodeMulx(const std::uint8_t* data, const std::size_t length) { + std::size_t vex = 0; + while (vex < length && (data[vex] == 0x2E || data[vex] == 0x36 || data[vex] == 0x3E || data[vex] == 0x26 || data[vex] == 0x64 || data[vex] == 0x65 || data[vex] == 0x67)) ++vex; + if (vex + 5 > length || data[vex] != 0xC4 || (data[vex + 1] & 0x1F) != 2 || data[vex + 3] != 0xF6 || (data[vex + 2] & 3) != 3) + return std::nullopt; + if ((data[vex + 2] & 4) != 0) + throw CodegenException("MULX requires VEX.L=0"); + const auto extensions = static_cast(~data[vex + 1]); + const auto modrm = data[vex + 4]; + MulxOperands operands{ + static_cast(((modrm >> 3) & 7) | ((extensions & 0x80) >> 4)), + static_cast((~data[vex + 2] >> 3) & 15), + static_cast((modrm & 7) | ((extensions & 0x20) >> 2)), + (data[vex + 2] & 0x80) != 0, std::nullopt}; + if ((modrm >> 6) == 3) { + if (vex + 5 != length) throw CodegenException("MULX register operand has trailing bytes"); + } else { + const auto rex = static_cast(((extensions & 0x20) >> 5) | ((extensions & 0x40) >> 5)); + operands.Memory = DecodeMemoryOperand(data, length, vex + 4, rex, {data, data + vex}); + if (vex + 4 + operands.Memory->EncodedSize != length) + throw CodegenException("MULX memory operand has trailing bytes"); + } + return operands; +} + +void EmitMulx(StubBodyBuilder& body, const MulxOperands& operands) { + if (operands.Memory && operands.Memory->StackBase && (operands.Memory->Sib & 0x38) == 0x20 && (operands.Memory->RexIndexBase & 2) == 0) { + const auto displacement = static_cast(operands.Memory->Displacement); + if (displacement < 24 - kFrameSize && displacement + (operands.Wide ? 8 : 4) > -kFrameSize) + throw CodegenException("MULX source overlaps its spill frame"); + } + _stack(body, 0x8D, true, kStack, 8 - kFrameSize); + const std::array pushFlags{std::uint8_t{0x9C}}; + body.Raw(pushFlags); + _stack(body, 0x89, true, 0, 8); + _stack(body, 0x89, true, 2, 16); + _loadSource(body, operands); + const std::array multiply{_rex(operands.Wide, 0, 2), std::uint8_t{0xF7}, std::uint8_t{0xE2}}; + body.Raw(multiply); + _stack(body, 0x89, true, 0, 24); + _stack(body, 0x89, true, 2, 32); + _stack(body, 0x8B, true, 0, 8); + _stack(body, 0x8B, true, 2, 16); + if (operands.LowDestination != kStack) + _stack(body, 0x8B, operands.Wide, operands.LowDestination, 24); + if (operands.HighDestination != kStack) + _stack(body, 0x8B, operands.Wide, operands.HighDestination, 32); + const std::array popFlags{std::uint8_t{0x9D}}; + body.Raw(popFlags); + _stack(body, 0x8D, true, kStack, kFrameSize - 8); + if (operands.HighDestination == kStack || operands.LowDestination == kStack) + _stack(body, 0x8B, operands.Wide, kStack, (operands.HighDestination == kStack ? 32 : 24) - kFrameSize); +} + +} diff --git a/core/relinker/codegen/tests/MulxLoweringTests.cpp b/core/relinker/codegen/tests/MulxLoweringTests.cpp new file mode 100644 index 0000000000..d3d7a0528d --- /dev/null +++ b/core/relinker/codegen/tests/MulxLoweringTests.cpp @@ -0,0 +1,356 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if defined(_WIN32) +#include +#elif (defined(__linux__) || defined(__APPLE__)) && defined(__x86_64__) +#include +#endif + +namespace { + +using Bytes = std::vector; + +void require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +Bytes instruction(bool wide, std::uint8_t dst, std::uint8_t first, std::uint8_t second) { + return {0xC4, static_cast(0xE2 ^ ((dst & 8) << 4) ^ ((second & 8) << 2)), + static_cast((wide ? 0x80 : 0) | ((~first & 15) << 3) | 3), 0xF6, + static_cast(0xC0 | ((dst & 7) << 3) | (second & 7))}; +} + +void decoding() { + for (bool wide : {false, true}) { + for (std::uint8_t dst = 0; dst < 16; ++dst) { + for (std::uint8_t first = 0; first < 16; ++first) { + for (std::uint8_t second = 0; second < 16; ++second) { + const auto bytes = instruction(wide, dst, first, second); + const auto operands = Codegen::DecodeMulx(bytes.data(), bytes.size()); + require(operands && operands->Wide == wide && operands->HighDestination == dst && operands->LowDestination == first && operands->Source == second && !operands->Memory, "MULX register decoding changed an operand"); + } + } + } + } + const Bytes memory{0x64, 0x67, 0xC4, 0x02, 0xAB, 0xF6, 0x8C, 0xAC, 0x10, 0, 0, 0}; + const auto operands = Codegen::DecodeMulx(memory.data(), memory.size()); + require(operands && operands->HighDestination == 9 && operands->LowDestination == 10 && operands->Memory && operands->Memory->RexIndexBase == 3 && operands->Memory->Prefixes == Bytes{0x64, 0x67}, "MULX memory extensions or prefixes were lost"); + for (const Bytes bytes : {Bytes{0x90}, Bytes{0xC4, 0xE2, 0xF0}, Bytes{0xC4, 0xE1, 0xF3, 0xF6, 0xC0}, Bytes{0xC4, 0xE2, 0xF0, 0xF6, 0xC0}}) + require(!Codegen::DecodeMulx(bytes.data(), bytes.size()), "Unrelated or truncated opcode matched MULX"); + for (const Bytes bytes : {Bytes{0xC4, 0xE2, 0xF7, 0xF6, 0xC0}, Bytes{0xC4, 0xE2, 0xF3, 0xF6, 0x84}}) { + bool rejected = false; + try { (void)Codegen::DecodeMulx(bytes.data(), bytes.size()); } + catch (const Codegen::CodegenException&) { rejected = true; } + require(rejected, "Malformed MULX was accepted"); + } + const Bytes overflow{0xC4, 0xE2, 0xF3, 0xF6, 0x84, 0x24, 0xFF, 0xFF, 0xFF, 0x7F}; + bool rejected = false; + try { + Codegen::StubBodyBuilder body; + Codegen::EmitMulx(body, *Codegen::DecodeMulx(overflow.data(), overflow.size())); + } catch (const Codegen::CodegenException&) { rejected = true; } + require(rejected, "An overflowing stack displacement was silently wrapped"); + for (const bool wide : {false, true}) { + for (const auto [displacement, rejects32, rejects64] : {std::tuple{-199, false, true}, {-195, true, true}, {-192, true, true}, {-184, true, true}, {-176, true, true}, {-169, true, true}, {-168, false, false}}) { + Bytes site{0xC4, 0xE2, static_cast(wide ? 0xF3 : 0x73), 0xF6, 0x84, 0x24}; + for (unsigned shift = 0; shift < 32; shift += 8) + site.push_back(static_cast(static_cast(displacement) >> shift)); + rejected = false; + try { + Codegen::StubBodyBuilder body; + Codegen::EmitMulx(body, *Codegen::DecodeMulx(site.data(), site.size())); + } catch (const Codegen::CodegenException&) { rejected = true; } + require(rejected == (wide ? rejects64 : rejects32), "MULX spill alias detection has an incorrect boundary"); + } + } +} + +void matching() { + const auto bytes = instruction(true, 0, 1, 6); + const auto intel = Codegen::MakeAmd64OnlyInstructionMatcher(); + const auto rosetta = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); + require(!intel->Match(bytes.data(), bytes.size()), "Intel mode rewrote MULX"); + const auto match = rosetta->Match(bytes.data(), bytes.size()); + require(match && match->InstructionName == "MULX" && match->Lowering == Codegen::Amd64OnlyLowering::Trampoline && !match->Optional, "Rosetta mode did not require a MULX trampoline"); + const std::array, 2> sequence{bytes, bytes}; + require(rosetta->MatchSequence(sequence, {}).has_value(), "A MULX sequence could not move out of line"); + const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); + for (const Bytes site : {bytes, Bytes{0xC4, 0xE2, 0xF3, 0xF6, 0x05, 0x40, 0, 0, 0}}) { + Bytes file(0x300, 0xCC); + std::copy(site.begin(), site.end(), file.begin() + 0x200); + const Domain::ProgramHeader segment{1, 5, 0x200, 0x1000, 0, site.size(), site.size(), 16}; + const auto converted = converter->Convert(file, {segment}); + require(converted.Trampolines.size() == 1 && converted.Reports.size() == 1 && converted.Reports[0].InstructionName == "MULX", "Converter did not report a MULX trampoline"); + require(converted.Trampolines[0].Offset == 0x200 && converted.Trampolines[0].Length == site.size(), "Converter changed the MULX site extent"); + require(converted.Bytes == file, "MULX conversion changed the input image before patching"); + if (site.size() == 9) { + const auto& relocations = converted.Trampolines[0].Relocations; + require(relocations.size() == 1 && relocations[0].SiteTarget == 9 + 0x40, "MULX lost its RIP-relative memory target"); + } + const auto intelConverted = Codegen::MakeAmd64OnlyConverter()->Convert(file, {segment}); + require(intelConverted.Reports.empty() && intelConverted.Bytes == file, "Intel conversion rewrote MULX"); + } +} + +#if (defined(_WIN32) || defined(__linux__) || defined(__APPLE__)) && defined(__x86_64__) + +struct State { + std::array Input{}; + std::array Output{}; + std::uint64_t Flags = 0; + std::array RedZone{}; + std::uint64_t InputFlags = 0x202; +}; + +class Execution { +public: + Execution(const Bytes& site, std::uint64_t memoryValue) { + auto operands = Codegen::DecodeMulx(site.data(), site.size()); + require(operands.has_value(), "Execution site was not MULX"); + for (auto reg : {3, 5, 6, 7, 12, 13, 14, 15}) push(static_cast(reg)); +#if defined(_WIN32) + bytes.insert(bytes.end(), {0x48, 0x89, 0xCF}); +#endif + memory(0x8D, 4, 4, -128); + rip({0x48, 0x89, 0x3D}, 0); + rip({0x48, 0x89, 0x25}, 8); + memory(0x89, 4, 7, 32); + memory(0x8B, 0, 7, 49 * 8); + push(0); + bytes.push_back(0x9D); + bytes.insert(bytes.end(), {0x48, 0xB8}); + append64(kSentinel); + for (int displacement = -128; displacement < 0; displacement += 8) + memory(0x89, 0, 4, displacement); + memory(0x8B, 0, 7, 16 * 8); + memory(0x89, 0, 4, 64); + for (std::uint8_t reg = 0; reg < 16; ++reg) + if (reg != 4 && reg != 7) memory(0x8B, reg, 7, reg * 8); + memory(0x8B, 7, 7, 7 * 8); + const auto siteOffset = bytes.size(); + if (operands->Memory && operands->Memory->RipRelative) { + auto relocated = site; + const auto displacement = static_cast(4096 + 24 - (siteOffset + site.size())); + std::memcpy(relocated.data() + relocated.size() - 4, &displacement, sizeof(displacement)); + operands = Codegen::DecodeMulx(relocated.data(), relocated.size()); + } + Codegen::StubBodyBuilder builder; + Codegen::EmitMulx(builder, *operands); + auto body = builder.Finish(); + const auto stubOffset = bytes.size(); + Codegen::ApplyStubRelocations(body.Bytes, body.Relocations, siteOffset, stubOffset, 0); + bytes.insert(bytes.end(), body.Bytes.begin(), body.Bytes.begin() + static_cast(body.ReturnBranchOffset)); + rip({0x48, 0x89, 0x25}, 16); + rip({0x48, 0x8B, 0x25}, 8); + memory(0x8D, 4, 4, -256); + for (std::uint8_t reg = 0; reg < 16; ++reg) if (reg != 4) push(reg); + bytes.push_back(0x9C); + rip({0x48, 0x8B, 0x3D}, 0); + memory(0x8B, 0, 4, 0); + memory(0x89, 0, 7, 32 * 8); + int offset = 8; + for (int reg = 15; reg >= 0; --reg) { + if (reg == 4) continue; + memory(0x8B, 0, 4, offset); + memory(0x89, 0, 7, (16 + reg) * 8); + offset += 8; + } + rip({0x48, 0x8B, 0x05}, 16); + memory(0x89, 0, 7, 20 * 8); + for (int slot = 0; slot < 16; ++slot) { + memory(0x8B, 0, 4, 256 + 128 - 128 + slot * 8); + memory(0x89, 0, 7, (33 + slot) * 8); + } + rip({0x48, 0x8B, 0x25}, 8); + memory(0x8D, 4, 4, 128); + for (auto reg : {15, 14, 13, 12, 7, 6, 5, 3}) pop(static_cast(reg)); + bytes.push_back(0xC3); + require(bytes.size() < 4096, "Execution wrapper exceeded its code page"); +#if defined(_WIN32) + code = static_cast(VirtualAlloc(nullptr, 8192, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)); + require(code != nullptr, "Cannot allocate execution test pages"); +#else + code = static_cast(mmap(nullptr, 8192, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + require(code != MAP_FAILED, "Cannot allocate execution test pages"); +#endif + std::memcpy(code, bytes.data(), bytes.size()); + std::memcpy(code + 4096 + 24, &memoryValue, sizeof(memoryValue)); +#if defined(_WIN32) + DWORD previous = 0; + require(VirtualProtect(code, 4096, PAGE_EXECUTE_READ, &previous) != 0, "Cannot make MULX test code executable"); + require(FlushInstructionCache(GetCurrentProcess(), code, bytes.size()) != 0, "Cannot flush execution test code"); +#else + require(mprotect(code, 4096, PROT_READ | PROT_EXEC) == 0, "Cannot make MULX test code executable"); +#endif + } + + ~Execution() { +#if defined(_WIN32) + if (code) VirtualFree(code, 0, MEM_RELEASE); +#else + if (code) munmap(code, 8192); +#endif + } + + void Run(State& state) const { reinterpret_cast(code)(&state); } + const void* Memory() const { return code + 4096 + 24; } + static constexpr std::uint64_t kSentinel = 0xBADC0FFEE0DDF00Dull; + +private: + Bytes bytes; + std::uint8_t* code = nullptr; + + void append32(std::int32_t value) { + for (unsigned shift = 0; shift < 32; shift += 8) bytes.push_back(static_cast(static_cast(value) >> shift)); + } + void append64(std::uint64_t value) { + for (unsigned shift = 0; shift < 64; shift += 8) bytes.push_back(static_cast(value >> shift)); + } + void rip(std::initializer_list opcode, int slot) { + bytes.insert(bytes.end(), opcode); + append32(static_cast(4096 + slot - (bytes.size() + 4))); + } + void push(std::uint8_t reg) { + if (reg >= 8) bytes.push_back(0x41); + bytes.push_back(static_cast(0x50 | (reg & 7))); + } + void pop(std::uint8_t reg) { + if (reg >= 8) bytes.push_back(0x41); + bytes.push_back(static_cast(0x58 | (reg & 7))); + } + void memory(std::uint8_t opcode, std::uint8_t reg, std::uint8_t base, int displacement) { + bytes.insert(bytes.end(), {static_cast(0x48 | ((reg & 8) >> 1) | ((base & 8) >> 3)), opcode, + static_cast(0x80 | ((reg & 7) << 3) | (base & 7))}); + if ((base & 7) == 4) bytes.push_back(0x24); + append32(displacement); + } +}; + +std::pair product(std::uint64_t first, std::uint64_t second, bool wide) { + if (!wide) { + const auto result = (first & 0xFFFFFFFFull) * (second & 0xFFFFFFFFull); + return {result >> 32, result & 0xFFFFFFFFull}; + } + const auto low = (first & 0xFFFFFFFFull) * (second & 0xFFFFFFFFull); + const auto middle = (first >> 32) * (second & 0xFFFFFFFFull) + (low >> 32); + const auto carry = (first & 0xFFFFFFFFull) * (second >> 32) + (middle & 0xFFFFFFFFull); + return {(first >> 32) * (second >> 32) + (middle >> 32) + (carry >> 32), + (carry << 32) | (low & 0xFFFFFFFFull)}; +} + +void check(const State& state, const Codegen::MulxOperands& operands, std::uint64_t source) { + const auto [high, low] = product(state.Input[2], source, operands.Wide); + for (std::size_t reg = 0; reg < 16; ++reg) { + const auto expected = reg == operands.HighDestination ? high : reg == operands.LowDestination ? low : state.Input[reg]; + require(state.Output[reg] == expected, "MULX changed an unrelated register or produced an incorrect result"); + } + require((state.Flags & 0x8D5u) == (state.InputFlags & 0x8D5u), "MULX changed arithmetic flags"); + for (const auto value : state.RedZone) require(value == Execution::kSentinel, "MULX overwrote the caller's red zone"); +} + +State initialState() { + State state; + for (std::size_t reg = 0; reg < 16; ++reg) state.Input[reg] = 0xABCD123456789000ull + reg * 0x1234567ull; + return state; +} + +void execution() { + for (bool wide : {false, true}) { + for (std::uint8_t high = 0; high < 16; ++high) { + for (std::uint8_t low = 0; low < 16; ++low) { + for (std::uint8_t source = 0; source < 16; ++source) { + const auto site = instruction(wide, high, low, source); + Execution executable(site, 0); + for (int variant = 0; variant < 8; ++variant) { + auto state = initialState(); + if (variant == 1) state.Input.fill(0); + if (variant == 2) state.Input.fill(~std::uint64_t{0}); + if (variant == 3) state.Input[2] = 0x100000001ull; + if (variant >= 4) { + auto seed = static_cast(variant * 4096 + high * 256 + low * 16 + source); + for (auto& value : state.Input) { + seed ^= seed << 13; + seed ^= seed >> 7; + seed ^= seed << 17; + value = seed; + } + } + state.InputFlags |= variant % 2 == 0 ? 0x8D5 : 0; + executable.Run(state); + check(state, *Codegen::DecodeMulx(site.data(), site.size()), state.Input[source]); + } + } + } + } + for (std::uint8_t base = 0; base < 16; ++base) { + for (std::uint8_t high = 0; high < 16; ++high) { + for (std::uint8_t low = 0; low < 16; ++low) { + auto site = instruction(wide, high, low, base); + site[4] = static_cast(0x80 | ((high & 7) << 3) | (base & 7)); + if ((base & 7) == 4) site.push_back(0x24); + site.insert(site.end(), {static_cast(base == 4 ? 64 : 16), 0, 0, 0}); + constexpr std::uint64_t value = 0xFEDCBA9876543210ull; + Execution executable(site, value); + auto state = initialState(); + state.Input[base] = reinterpret_cast(executable.Memory()) - 16; + state.Output[0] = value; + state.InputFlags |= 0x895; + executable.Run(state); + check(state, *Codegen::DecodeMulx(site.data(), site.size()), value); + } + } + } + for (const Bytes tail : {Bytes{0x06}, Bytes{0x46, 0x10}, Bytes{0x84, 0x24, 0x40, 0, 0, 0}, Bytes{0x04, 0xCA}, Bytes{0x85, 0x10, 0, 0, 0}, Bytes{0x05, 0, 0, 0, 0}}) { + Bytes site{0xC4, 0xE2, static_cast(wide ? 0xF3 : 0x73), 0xF6}; + site.insert(site.end(), tail.begin(), tail.end()); + constexpr std::uint64_t value = 0xFEDCBA9876543210ull; + Execution executable(site, value); + auto state = initialState(); + state.Input[6] = reinterpret_cast(executable.Memory()) - (tail[0] == 0x46 ? 16 : 0); + state.Input[2] = reinterpret_cast(executable.Memory()) - 3 * 8; + state.Input[1] = 3; + state.Input[5] = reinterpret_cast(executable.Memory()) - 16; + state.Output[0] = value; + executable.Run(state); + check(state, *Codegen::DecodeMulx(site.data(), site.size()), value); + } + const auto site = instruction(wide, 2, 0, 2); + Execution executable(site, 0); + for (std::uint64_t bits = 0; bits < 64; ++bits) { + auto state = initialState(); + for (unsigned bit = 0; bit < 6; ++bit) + if ((bits >> bit) & 1) state.InputFlags |= std::uint64_t{1} << std::array{0, 2, 4, 6, 7, 11}[bit]; + executable.Run(state); + check(state, *Codegen::DecodeMulx(site.data(), site.size()), state.Input[2]); + } + } + std::cout << "Executed 73,868 MULX cases with register, flag and red-zone checks\n"; +} +#else +void execution() { std::cout << "MULX execution checks require an x86-64 build\n"; } +#endif + +} + +int main() { + try { + decoding(); + matching(); + execution(); + std::cout << "MULX lowering checks passed\n"; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/relinker/relinker/tests/test_mulx_conversion.py b/core/relinker/relinker/tests/test_mulx_conversion.py new file mode 100644 index 0000000000..7e133ef319 --- /dev/null +++ b/core/relinker/relinker/tests/test_mulx_conversion.py @@ -0,0 +1,51 @@ +from pathlib import Path +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import guest_fixture, main_fixture +from test_to_rosetta import executable + + +def main(): + relinker = Path(sys.argv[1]).resolve() + sites = (bytes.fromhex("c4e273f6c6"), bytes.fromhex("c442b3f6c2"), + bytes.fromhex("c4e2f3f6442440"), bytes.fromhex("c4e2f3f60540000000")) + with tempfile.TemporaryDirectory(prefix="anyps5-mulx-") as directory: + work = Path(directory) + for guest in (False, True): + for macos in (False, True): + for conversion in ((), ("--to-intel",), ("--to-rosetta",)): + for index, site in enumerate(sites): + case = work / f"{guest}-{macos}-{conversion}-{index}" + case.mkdir() + source = case / "input.elf" + output = case / "output" + options = ["--macos"] if macos else [] + if guest: + modules = case / "sce_module" + modules.mkdir() + module = modules / "sample.prx" + original = guest_fixture(site) + module.write_bytes(original) + source.write_bytes(main_fixture()) + else: + options.append("--skip-sce-module") + source.write_bytes(executable(site + b"\xc3")) + result = subprocess.run([str(relinker), *options, *conversion, str(source), str(output)], + capture_output=True, text=True, timeout=10) + assert result.returncode == 0, (case.name, result.stdout, result.stderr) + artifact = case / "app0/sce_module/sample.prx.guest.prx" if guest else output + data = artifact.read_bytes() + assert data[:4] == (bytes.fromhex("cffaedfe") if macos else b"\x7fELF"), case.name + lowered = conversion == ("--to-rosetta",) + assert (site not in data) == lowered, (case.name, "MULX instruction presence") + if not guest: + assert ("MULX" in result.stdout) == lowered, (case.name, result.stdout) + if guest: + assert module.read_bytes() == original, case.name + print("48 executable/guest MULX conversions passed across Linux and macOS output formats") + + +if __name__ == "__main__": + main() diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 48d5f103f5..8a40427560 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -2,6 +2,8 @@ ### Functional +- Execution of [MULX lowering](../../../core/relinker/codegen/src/x86/MulxLowering.cpp) under Rosetta and precise fault delivery inside its spill sequence have not been verified. The Rosetta host observations below predate this lowering for MULX. The stub uses 192 bytes below RSP, leaving the 128-byte red zone intact; direct RSP references to its spill slots throw, while aliases through other registers are not detected. A 64-bit stack-relative displacement that exceeds signed disp32 after reserving the spill frame throws. + - [Packed relative relocations](../../../core/relinker/relinker/src/parsing/RelrRelocations.cpp) require the table to fit in one file-backed PT_LOAD segment and each 8-byte storage unit to fit in one load segment. Units spanning load segments, overlapping RELR units and overlaps with RELA relocations are rejected; relocation composition is not implemented. - The [syscall scanner](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) decodes executable ranges linearly without code/data boundaries. Embedded data between routines can fail decoding or be reported as a forbidden syscall ([#7547](https://github.com/boykopovar/AnyPS5/issues/7547)). Unwind ranges alone omit real code. The [synthetic fixture](../../../core/relinker/relinker/tests/CodeDataScanningTests.cpp) characterizes these rejections and checks a reachable routine beyond the gap; it does not resolve the boundary problem. From c3814cb7a48ea61c272ef052ca0818e9582898e6 Mon Sep 17 00:00:00 2001 From: tkcm01 <75453983+tkcm01@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:11:08 -0400 Subject: [PATCH 051/160] test(agc): expect no commit charge on macOS ReadCommitCharge reads /proc/meminfo outside Windows, and macOS has no /proc, so it returns nothing there and page 2 leaves the commit part of its COMMIT line out, as it does for any source the run doesn't have. The statistics test required a reading on every system, and the page 2 sampler test required the first sample to hold one. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp | 2 +- .../libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp | 4 ++++ docs/user/USAGE.md | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp index a300960e16..d829179197 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp @@ -689,7 +689,7 @@ void pageTwoSamplerTest(const Device& device) { const auto present = [&](std::chrono::milliseconds at, std::uint64_t address, std::uint64_t drops) { overlay.NotePresent(start + at, VkExtent2D{640, 360}, AgcDriver::OverlayPresentInfo{address, VK_PRESENT_MODE_FIFO_KHR, drops}); }; present(0ms, 0x1000, 7); const auto& sample = overlay.DetailSample(); - Require(sample.commit.has_value() && sample.unimplementedTotal.has_value() && sample.presentMode == "fifo" && sample.pipelinesCompiling == std::optional{0}, "page 2's first sample does not read the commit charge, the unimplemented calls, the compile backlog and the present mode"); + Require(sample.commit.has_value() == AgcDriver::ReadCommitCharge().has_value() && sample.unimplementedTotal.has_value() && sample.presentMode == "fifo" && sample.pipelinesCompiling == std::optional{0}, "page 2's first sample does not read the commit charge where the system reports one, the unimplemented calls, the compile backlog and the present mode"); Require(!sample.flipsPerSecond && !sample.pageFaultsPerSecond && !sample.cpuCopyMegabytesPerSecond && sample.busy.empty() && sample.phases.empty(), "page 2's first sample shows rates before a second has passed"); Require(sample.hostImports.has_value() == (context.hostImportAlignment != 0), "the host import line does not follow the device's host import support"); std::thread([] { diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp index 316e631457..3de0005025 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp @@ -737,7 +737,11 @@ void operatingSystemTests() { require(!AgcDriver::ParseCommitCharge("CommitLimit: 8159204 kB\nCommitted_AS: 12 MB\n"), "a value in another unit is read as kB"); require(!AgcDriver::ParseCommitCharge(""), "an empty /proc/meminfo gives a commit charge"); const auto commit = AgcDriver::ReadCommitCharge(); +#ifdef __APPLE__ + require(!commit, "a commit charge is read on macOS, which reports none"); +#else require(commit && commit->used != 0 && commit->budget != 0, "the system's commit charge and limit cannot be read"); +#endif static std::uint8_t* volatile touched = nullptr; const auto before = AgcDriver::ReadPageFaults(); require(before.has_value(), "the process's page faults cannot be read"); diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 057f7551c6..d456fd79cc 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -437,7 +437,7 @@ Page 2 is wider, 46 characters, and has no graph. It reads its sources once a se | `HIT %` | `DRAW CACHE`: the share of the draw cache's lookups in the last second that found an entry whose stages all matched. `PIPELINES`: the share of the pipeline cache's lookups that found the pipeline. Each is left out for a second without lookups. | | `FLIPS/S`, `PRESENTS/S` | The title's flips per second against the frames presented per second. | | `DROPPED/S`, `REPEATED/S` | Per second: flips not presented because no swapchain image was free (the `dropped` count of the `[present]` line of `APS5_PROFILE_DRAW=1`), and presents of the same display buffer as the present before. | -| `COMMIT` | The system's commit charge against its commit limit, in GB: `CommitTotal` and `CommitLimit` of `GetPerformanceInfo` on Windows, `Committed_AS` and `CommitLimit` of `/proc/meminfo` on Linux, where the limit is enforced only with `vm.overcommit_memory=2`. Then `FAULTS/S`, the process's page faults per second, soft ones included: `PageFaultCount` of `GetProcessMemoryInfo` on Windows, minor plus major faults of `getrusage` on Linux. | +| `COMMIT` | The system's commit charge against its commit limit, in GB: `CommitTotal` and `CommitLimit` of `GetPerformanceInfo` on Windows, `Committed_AS` and `CommitLimit` of `/proc/meminfo` on Linux, where the limit is enforced only with `vm.overcommit_memory=2`. Then `FAULTS/S`, the process's page faults per second, soft ones included: `PageFaultCount` of `GetProcessMemoryInfo` on Windows, minor plus major faults of `getrusage` on Linux and macOS. macOS reports no commit charge, so the line shows only `FAULTS/S` there. | | `HOST IMPORTS` | Host memory imports in use, then their size against the import budget, in GB. Not shown on a device that can't import host memory. | | `CPU COPIES` | Guest memory copied per second by address-based resource builds, for the ranges that no host import or mirror serves, in MB of 2^20 bytes. `CAP`: the largest of those builds in the last second against the copy cap, `APS5_ADDRESS_COPY_MAX_MIB` (64 MiB unless set), past which a build stops the game. | | `UNIMPLEMENTED` | Calls per second to functions AnyPS5 doesn't implement, then their total since the start. Such a call throws an error, so a count above 0 means the title caught it and went on. | From 2b4efb1dc0332e89ebd9f9fb7a40f0db1b2b2be8 Mon Sep 17 00:00:00 2001 From: Filip Michalski Date: Wed, 28 Oct 2026 07:00:18 +0100 Subject: [PATCH 052/160] perf(shader): bypass identity fragment export mappings --- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index eb38969f9a..5f5f8a1efc 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -2051,15 +2051,21 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { auto value = ExportVector(ctx, data, exp, uintOutput); if (state.program.Resources().stage == IrShaderStage::Pixel && exp.kind == ExportTargetKind::Mrt) { if (exp.index >= 8u) throw std::runtime_error("fragment export target exceeds the runtime mapping table"); - std::array components{}; - for (std::uint32_t component = 0; component < components.size(); ++component) { - const auto index = state.registrationModules ? EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), EmitShaderDataDwordLoad(state, state.program.Metadata().bindings.RuntimeStateDword() + RuntimeAbi::StateExportMappings + exp.index / 4u), ConstantU32(state, exp.index % 4u * 8u + component * 2u)), 3u) : state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ExportBase + exp.index * 4u + component, component); - components[component] = state.module.AllocateId(); - state.module.AddFunction(spv::OpVectorExtractDynamic, uintOutput ? TypeU32(state) : TypeF32(state), components[component], value, index); - } - const auto mapped = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeConstruct, vectorType, mapped, components[0], components[1], components[2], components[3]); - value = mapped; + const auto exported = value; + const auto mapping = state.registrationModules ? Binary(state, spv::OpShiftRightLogical, TypeU32(state), EmitShaderDataDwordLoad(state, state.program.Metadata().bindings.RuntimeStateDword() + RuntimeAbi::StateExportMappings + exp.index / 4u), ConstantU32(state, exp.index % 4u * 8u)) : 0u; + const auto mapped = [&] { + std::array components{}; + for (std::uint32_t component = 0; component < components.size(); ++component) { + const auto index = state.registrationModules ? EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), mapping, ConstantU32(state, component * 2u)), 3u) : state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ExportBase + exp.index * 4u + component, component); + components[component] = state.module.AllocateId(); + state.module.AddFunction(spv::OpVectorExtractDynamic, uintOutput ? TypeU32(state) : TypeF32(state), components[component], exported, index); + } + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, vectorType, result, components[0], components[1], components[2], components[3]); + return result; + }; + constexpr std::uint32_t identityMapping = 0xe4u; + value = state.registrationModules ? EmitValueIfElse(state, EmitCompareU32Constant(state, spv::OpIEqual, EmitAndConstant(state, mapping, 0xffu), identityMapping), vectorType, [&] { return exported; }, mapped) : mapped(); if (exp.index == 0u && !uintOutput && AlphaToCoverage(state)) StoreAlphaCoverage(state, value); const auto mode = MrtOutputMode(state, exp); if (PixelInfo(state).mergeMask != 0u && exp.index == PixelInfo(state).mergeTarget) { From 8cf6594aff7d021cb323641e41c994a3c52b033c Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 28 Oct 2026 20:16:06 +1300 Subject: [PATCH 053/160] test(agc): round-trip a pixel request with a target blend and depth export bounds The serializer writes the target blend block and then the depth export bounds. No test carried both, so swapping the two writes passed ctest. --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index c2fc87c055..a319a84882 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -6253,6 +6253,17 @@ void verifyPixelRequestSerialization() { RecompileCacheKey::Build(withoutStencilRef, replayKey); require(key != replayKey && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(withoutStencilRef), "the stencil reference export did not change the static shader identity"); } + auto blendedDepth = request; + blendedDepth.context.pixel = pixel; + blendedDepth.context.pixel->depthExportEnable = true; + blendedDepth.context.pixel->depthExportMin = -0.25f; + blendedDepth.context.pixel->depthExportMax = 0.75f; + blendedDepth.context.pixel->targetMerge.blend = {true, true, 6u, 7u, 1u, 2u, 3u, 4u, 0xau, {0x3f800000u, 0x40000000u, 0x40400000u, 0x40800000u}}; + const auto blendedReplay = serializer.Deserialize(serializer.Serialize(blendedDepth)); + require(blendedReplay.request.context.pixel.has_value() && fields(*blendedReplay.request.context.pixel) == fields(*blendedDepth.context.pixel), "a pixel request with a target blend and depth export bounds lost pixel fields in serialization"); + require(blendedReplay.request.context.pixel->targetMerge.blend == blendedDepth.context.pixel->targetMerge.blend, "the target blend did not round-trip beside the depth export bounds"); + require(blendedReplay.request.context.pixel->depthExportMin == -0.25f && blendedReplay.request.context.pixel->depthExportMax == 0.75f, "the depth export bounds did not round-trip beside the target blend"); + require(blendedReplay.request.context.vertex.has_value() && blendedReplay.request.context.vertex->fetchAttribReg == 17u && blendedReplay.request.context.memory.size() == 1u && blendedReplay.request.context.memory[0].guestAddress == 0x60000u, "a target blend with depth export bounds displaced the following guest context"); request.context.pixel.reset(); request.shader.stage = ShaderStage::Vertex; const auto withoutPixel = serializer.Deserialize(serializer.Serialize(request)); From 83290fb482c94e6d28645646a88b12e8572396ab Mon Sep 17 00:00:00 2001 From: StoneDC <19905433+StoneDC@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:47:54 +0800 Subject: [PATCH 054/160] fix(tools): stage release notes before publishing outputs --- tools/prepare_release_notes.py | 21 ++++- tools/tests/test_package_release.py | 117 ++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 4 deletions(-) diff --git a/tools/prepare_release_notes.py b/tools/prepare_release_notes.py index 0077730684..ec5d27906a 100644 --- a/tools/prepare_release_notes.py +++ b/tools/prepare_release_notes.py @@ -2,6 +2,7 @@ import argparse import json import os import re +import secrets import stat import sys from pathlib import Path @@ -41,10 +42,22 @@ def prepare_notes(source, body, full): raise IsADirectoryError(str(destination)) if os.name == "nt" and destination.exists() and destination.stat().st_file_attributes & stat.FILE_ATTRIBUTE_READONLY: raise PermissionError(f"Release note destination is read-only: {destination}") - body.parent.mkdir(parents=True, exist_ok=True) - full.parent.mkdir(parents=True, exist_ok=True) - full.write_bytes(notes.encode("utf-8")) - body.write_bytes(body_text.encode("utf-8")) + staged = [] + try: + for destination, text in ((full, notes), (body, body_text)): + target = destination.resolve() + target.parent.mkdir(parents=True, exist_ok=True) + temporary = target.parent / (".release-notes-" + secrets.token_hex(16)) + with temporary.open("xb") as output: + staged.append((temporary, target)) + if target.exists(): + temporary.chmod(stat.S_IMODE(target.stat().st_mode)) + output.write(text.encode("utf-8")) + for temporary, target in staged: + os.replace(temporary, target) + finally: + for temporary, _ in staged: + temporary.unlink(missing_ok=True) def main(argv=None): diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index 9c061dfaf9..22a974204c 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -885,5 +885,122 @@ class ReleaseMainTests(unittest.TestCase): self.assertEqual(err.errors, "backslashreplace") + +class ReleaseNotesStagingTests(unittest.TestCase): + def test_partial_write_to_either_output_preserves_both_old_files(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + source, body, full = (root / name for name in ("notes.json", "body", "full")) + source.write_text(json.dumps({"body": "new notes"}), encoding="utf-8") + original_open = Path.open + for fail_at in (1, 2): + body.write_bytes(b"old body") + full.write_bytes(b"old full") + writes = 0 + + class PartialWriter: + def __init__(self, stream): + self.stream = stream + + def __enter__(self): + self.stream.__enter__() + return self + + def __exit__(self, *args): + return self.stream.__exit__(*args) + + def write(self, data): + self.stream.write(data[:3]) + raise OSError("injected partial write") + + def opening(path, mode="r", *args, **kwargs): + nonlocal writes + stream = original_open(path, mode, *args, **kwargs) + if "w" in mode or "x" in mode: + writes += 1 + if writes == fail_at: + return PartialWriter(stream) + return stream + + with patch.object(Path, "open", opening), self.assertRaisesRegex(OSError, "injected partial write"): + prepare_release_notes.prepare_notes(source, body, full) + self.assertEqual(body.read_bytes(), b"old body") + self.assertEqual(full.read_bytes(), b"old full") + self.assertEqual(list(root.glob(".release-notes-*")), []) + prepare_release_notes.prepare_notes(source, body, full) + self.assertEqual(body.read_bytes(), b"new notes") + self.assertEqual(full.read_bytes(), b"new notes") + + @unittest.skipIf(os.name == "nt", "requires POSIX file size limits") + def test_real_file_size_limit_preserves_outputs_and_removes_staging(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + source, body, full = (root / name for name in ("notes.json", "body", "full")) + source.write_text(json.dumps({"body": "N" * 500}), encoding="utf-8") + body.write_bytes(b"old body") + full.write_bytes(b"old full") + program = ("import resource,signal,sys\nfrom pathlib import Path\n" + "sys.path.insert(0,sys.argv[1])\nimport prepare_release_notes\n" + "resource.setrlimit(resource.RLIMIT_FSIZE,(64,64))\nsignal.signal(signal.SIGXFSZ,signal.SIG_IGN)\n" + "try: prepare_release_notes.prepare_notes(*(Path(p) for p in sys.argv[2:]))\nexcept OSError: sys.exit(2)\n") + completed = subprocess.run([sys.executable, "-c", program, str(Path(prepare_release_notes.__file__).parent), str(source), str(body), str(full)], capture_output=True, timeout=10) + self.assertEqual(completed.returncode, 2, completed.stderr) + self.assertEqual(body.read_bytes(), b"old body") + self.assertEqual(full.read_bytes(), b"old full") + self.assertEqual(list(root.glob(".release-notes-*")), []) + + @unittest.skipIf(os.name == "nt", "requires POSIX mode bits and umask") + def test_existing_modes_and_new_file_umask_are_retained(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + source, body, full = (root / name for name in ("notes.json", "body", "full")) + source.write_text(json.dumps({"body": "new"}), encoding="utf-8") + body.write_bytes(b"old") + body.chmod(0o640) + old_mask = os.umask(0o027) + try: + prepare_release_notes.prepare_notes(source, body, full) + finally: + os.umask(old_mask) + self.assertEqual(stat.S_IMODE(body.stat().st_mode), 0o640) + self.assertEqual(stat.S_IMODE(full.stat().st_mode), 0o640) + + def test_output_symlink_is_preserved(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + source, body, full, target = (root / name for name in ("notes.json", "body", "full", "target")) + source.write_text(json.dumps({"body": "new"}), encoding="utf-8") + target.write_bytes(b"old") + try: + full.symlink_to(target) + except OSError as error: + if getattr(error, "winerror", None) == 1314: + self.skipTest("needs symbolic-link creation privilege") + raise + prepare_release_notes.prepare_notes(source, body, full) + self.assertTrue(full.is_symlink()) + self.assertEqual(target.read_bytes(), b"new") + + def test_promotion_failure_leaves_other_destination_intact(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + source, body, full = (root / name for name in ("notes.json", "body", "full")) + source.write_text(json.dumps({"body": "new"}), encoding="utf-8") + body.write_bytes(b"old body") + full.write_bytes(b"old full") + original_replace = os.replace + + def replace(first, second): + if second == body.resolve(): + raise OSError("second promotion failed") + return original_replace(first, second) + + with patch.object(prepare_release_notes.os, "replace", replace), self.assertRaisesRegex(OSError, "second promotion failed"): + prepare_release_notes.prepare_notes(source, body, full) + self.assertEqual(body.read_bytes(), b"old body") + self.assertEqual(full.read_bytes(), b"new") + self.assertEqual(list(root.glob(".release-notes-*")), []) + + if __name__ == "__main__": unittest.main() From c5091d36f8908ab6c2318cba52882c45942dd54e Mon Sep 17 00:00:00 2001 From: rooklz <175737347+rooklz@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:30:39 +0000 Subject: [PATCH 055/160] fix(tools): report unusable symbolize_samples inputs as usage errors --- tools/symbolize_samples.py | 7 ++++++ tools/tests/test_symbolize_samples.py | 31 ++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/tools/symbolize_samples.py b/tools/symbolize_samples.py index 503f52f4de..811d77d5e5 100644 --- a/tools/symbolize_samples.py +++ b/tools/symbolize_samples.py @@ -306,6 +306,13 @@ def main(argv=None): parser.add_argument("--thread", help="keep only this thread of an APS5_SAMPLE_THREADS file") parser.add_argument("--unverified", action="store_true", help="also symbolize modules the samples have no '# module' record for") args = parser.parse_args(argv) + try: + return symbolize(parser, args) + except (OSError, ValueError) as error: + parser.error(str(error)) + + +def symbolize(parser, args): if args.out: output = Path(args.out) for source in (args.samples, args.since): diff --git a/tools/tests/test_symbolize_samples.py b/tools/tests/test_symbolize_samples.py index e675dd6f47..3d75957d75 100644 --- a/tools/tests/test_symbolize_samples.py +++ b/tools/tests/test_symbolize_samples.py @@ -248,9 +248,12 @@ class SymbolizeSamplesTests(unittest.TestCase): module = self.libs / "libSceAgcDriver.prx" missing = self.root / "missing-libs" original = module.read_bytes() - with self.assertRaises(FileNotFoundError): + errors = io.StringIO() + with contextlib.redirect_stderr(errors), self.assertRaises(SystemExit) as raised: symbolize_samples.main([str(samples), "--libs", str(self.libs), "--libs", str(missing), "--out", str(module)]) + self.assertEqual(raised.exception.code, 2) + self.assertIn(missing.name, errors.getvalue()) self.assertEqual(module.read_bytes(), original) def test_distinct_output_preserves_sample_captures(self): @@ -424,6 +427,32 @@ class SymbolizeSamplesTests(unittest.TestCase): with self.assertRaisesRegex(ValueError, r"bad\.collapsed\.txt:2:"): symbolize_samples.load(bad) + def test_unusable_inputs_are_usage_errors(self): + good = self.root / "good.collapsed.txt" + good.write_text("libSceAgcDriver.prx+0x1011 1\n", encoding="utf-8") + bad = self.root / "bad.collapsed.txt" + bad.write_text("# header\nlibSceAgcDriver.prx+0x1011 many\n", encoding="utf-8") + binary = self.root / "binary.collapsed.txt" + binary.write_bytes(b"\xff\xfe\n") + cases = { + "missing samples": ([self.root / "missing.txt", "--libs", self.libs], "missing.txt"), + "undecodable samples": ([binary, "--libs", self.libs], "codec"), + "malformed samples": ([bad, "--libs", self.libs], "bad.collapsed.txt:2:"), + "malformed since": ([good, "--since", bad, "--libs", self.libs], "bad.collapsed.txt:2:"), + "missing libs": ([good, "--libs", self.root / "missing-libs"], "missing-libs"), + "unwritable out": ([good, "--libs", self.libs, "--out", self.root / "missing" / "out.txt"], "out.txt"), + } + for name, (arguments, message) in cases.items(): + with self.subTest(name): + errors = io.StringIO() + with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(errors), \ + self.assertRaises(SystemExit) as raised: + symbolize_samples.main([str(argument) for argument in arguments]) + self.assertEqual(raised.exception.code, 2) + self.assertIn("error:", errors.getvalue()) + self.assertIn(message, errors.getvalue()) + self.assertNotIn("Traceback", errors.getvalue()) + def test_removed_module_warns_once_and_other_modules_still_resolve(self): missing = self.libs / "removed.prx" build_pe(missing) From 77a4c6f0660453829c5abb77dafc84b31a11bbfb Mon Sep 17 00:00:00 2001 From: Matheus Herberts <163153928+Mherberts@users.noreply.github.com> Date: Wed, 28 Oct 2026 07:53:33 -0300 Subject: [PATCH 056/160] docs(relinker): record MULX testing under Rosetta --- docs/dev/debt/relinker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 8a40427560..8b3970d349 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -2,7 +2,7 @@ ### Functional -- Execution of [MULX lowering](../../../core/relinker/codegen/src/x86/MulxLowering.cpp) under Rosetta and precise fault delivery inside its spill sequence have not been verified. The Rosetta host observations below predate this lowering for MULX. The stub uses 192 bytes below RSP, leaving the 128-byte red zone intact; direct RSP references to its spill slots throw, while aliases through other registers are not detected. A 64-bit stack-relative displacement that exceeds signed disp32 after reserving the spill frame throws. +- Execution of a title using [MULX lowering](../../../core/relinker/codegen/src/x86/MulxLowering.cpp) under Rosetta and precise fault delivery inside its spill sequence have not been verified. The lowering test passes under Rosetta on an M5 with macOS 26.2. The Rosetta host observations below predate this lowering for MULX. The stub uses 192 bytes below RSP, leaving the 128-byte red zone intact; direct RSP references to its spill slots throw, while aliases through other registers are not detected. A 64-bit stack-relative displacement that exceeds signed disp32 after reserving the spill frame throws. - [Packed relative relocations](../../../core/relinker/relinker/src/parsing/RelrRelocations.cpp) require the table to fit in one file-backed PT_LOAD segment and each 8-byte storage unit to fit in one load segment. Units spanning load segments, overlapping RELR units and overlaps with RELA relocations are rejected; relocation composition is not implemented. From 1bf07fbc26ba8cafccf5508579a82532477a3d57 Mon Sep 17 00:00:00 2001 From: rooklz <175737347+rooklz@users.noreply.github.com> Date: Wed, 28 Oct 2026 00:52:46 +0000 Subject: [PATCH 057/160] fix(tools): refuse stubgen outputs that alias on case-insensitive filesystems --- tools/stubgen.py | 28 ++++++++++++++-- tools/tests/test_stubgen.py | 67 +++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/tools/stubgen.py b/tools/stubgen.py index 9ec25fe6ab..04364b2e68 100644 --- a/tools/stubgen.py +++ b/tools/stubgen.py @@ -177,9 +177,33 @@ def generate(module, nids, db, td_path=None, td_lib=None): return (code, td_text) +def case_insensitive_directory(directory): + directory = Path(directory) + while not directory.is_dir(): + if directory.parent == directory: + return False + directory = directory.parent + try: + handle, name = tempfile.mkstemp(dir=str(directory)) + except OSError: + return False + try: + os.close(handle) + flipped = Path(name).with_name(Path(name).name.swapcase()) + return flipped.name != Path(name).name and flipped.exists() + finally: + os.unlink(name) + + def same_file(left, right): - left, right = Path(left), Path(right) - return left.resolve() == right.resolve() or (left.exists() and right.exists() and left.samefile(right)) + left, right = Path(left).resolve(), Path(right).resolve() + if left == right: + return True + if left.exists() and right.exists() and left.samefile(right): + return True + if str(left).casefold() == str(right).casefold(): + return case_insensitive_directory(left.parent) + return False def write_outputs(outputs): diff --git a/tools/tests/test_stubgen.py b/tools/tests/test_stubgen.py index 70c56941b4..37a34f71fc 100644 --- a/tools/tests/test_stubgen.py +++ b/tools/tests/test_stubgen.py @@ -322,5 +322,72 @@ class ReadOnlyOutputTests(unittest.TestCase): self.assertEqual(err.newline, "\n") +class CaseVariantOutputTests(unittest.TestCase): + def test_case_variant_outputs_are_refused_before_writing(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + if not stubgen.case_insensitive_directory(root): + self.skipTest("the output directory is case-sensitive") + nids = root / "nids.txt" + nids.write_text("AAAAAAAAAAA\n", encoding="utf-8") + cases = [["--module", "libSceExample", "--nids", str(nids), "--out", str(root / "REPORT.TXT"), + "--td-out", str(root / "report.txt")]] + try: + link = root / "code-link.txt" + link.symlink_to(root / "REPORT.TXT") + except OSError: + link = None + if link is not None: + cases.append(["--module", "libSceExample", "--nids", str(nids), "--out", str(link), + "--td-out", str(root / "report.txt")]) + for arguments in cases: + with self.subTest(arguments=arguments): + with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as error: + stubgen.main(arguments) + self.assertEqual(error.exception.code, 2) + self.assertFalse((root / "REPORT.TXT").exists()) + self.assertFalse((root / "report.txt").exists()) + + def test_case_variant_output_with_an_existing_file_stays_refused(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + if not stubgen.case_insensitive_directory(root): + self.skipTest("the output directory is case-sensitive") + nids = root / "nids.txt" + nids.write_text("AAAAAAAAAAA\n", encoding="utf-8") + debt = root / "report.txt" + original = "existing debt\n" + debt.write_text(original, encoding="utf-8") + arguments = ["--module", "libSceExample", "--nids", str(nids), "--out", str(root / "REPORT.TXT"), + "--td-out", str(debt)] + with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as error: + stubgen.main(arguments) + self.assertEqual(error.exception.code, 2) + self.assertEqual(debt.read_text(encoding="utf-8"), original) + + def test_case_variant_outputs_stay_distinct_on_case_sensitive_directories(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + upper, lower = root / "REPORT.TXT", root / "report.txt" + try: + link = root / "code-link.txt" + link.symlink_to(upper) + except OSError: + link = None + with patch.object(stubgen, "case_insensitive_directory", return_value=False): + self.assertFalse(stubgen.same_file(upper, lower)) + if link is not None: + self.assertFalse(stubgen.same_file(link, lower)) + if stubgen.case_insensitive_directory(root): + self.skipTest("the output directory is case-insensitive") + nids = root / "nids.txt" + nids.write_text("AAAAAAAAAAA\n", encoding="utf-8") + arguments = ["--module", "libSceExample", "--nids", str(nids), "--out", str(upper), + "--td-out", str(lower)] + self.assertEqual(stubgen.main(arguments), 0) + self.assertIn('APS5_EXPORT("AAAAAAAAAAA",', upper.read_text(encoding="utf-8")) + self.assertIn("unknown name, signature", lower.read_text(encoding="utf-8")) + + if __name__ == "__main__": unittest.main() From e0c050badce0b88adcbd996372db71e0881d50c2 Mon Sep 17 00:00:00 2001 From: rooklz <175737347+rooklz@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:24:10 +0000 Subject: [PATCH 058/160] fix(tools): resolve dangling output symlinks before the alias check --- tools/stubgen.py | 12 +++++++++++- tools/tests/test_stubgen.py | 3 +++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/tools/stubgen.py b/tools/stubgen.py index 04364b2e68..9496e7ae75 100644 --- a/tools/stubgen.py +++ b/tools/stubgen.py @@ -195,8 +195,18 @@ def case_insensitive_directory(directory): os.unlink(name) +def resolved_path(path): + path = Path(path) + for _ in range(40): + if not path.is_symlink(): + break + target = Path(os.readlink(path)) + path = target if target.is_absolute() else path.parent / target + return path.resolve() + + def same_file(left, right): - left, right = Path(left).resolve(), Path(right).resolve() + left, right = resolved_path(left), resolved_path(right) if left == right: return True if left.exists() and right.exists() and left.samefile(right): diff --git a/tools/tests/test_stubgen.py b/tools/tests/test_stubgen.py index 37a34f71fc..90475b13cc 100644 --- a/tools/tests/test_stubgen.py +++ b/tools/tests/test_stubgen.py @@ -368,6 +368,9 @@ class CaseVariantOutputTests(unittest.TestCase): def test_case_variant_outputs_stay_distinct_on_case_sensitive_directories(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) + missing = root / "MiXeD.CaSe" + if missing.resolve().name != missing.name: + self.skipTest("Path.resolve() normalizes the case of missing files") upper, lower = root / "REPORT.TXT", root / "report.txt" try: link = root / "code-link.txt" From dc1ceeb054723702d4ce87f46700824f057da037 Mon Sep 17 00:00:00 2001 From: rooklz <175737347+rooklz@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:39:50 +0000 Subject: [PATCH 059/160] fix(tools): compare stubgen output paths case-sensitively before the alias probe --- tools/stubgen.py | 14 ++++++++++++-- tools/tests/test_stubgen.py | 8 ++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/tools/stubgen.py b/tools/stubgen.py index 9496e7ae75..8558fa9c13 100644 --- a/tools/stubgen.py +++ b/tools/stubgen.py @@ -200,14 +200,24 @@ def resolved_path(path): for _ in range(40): if not path.is_symlink(): break - target = Path(os.readlink(path)) + target = Path(link_target(os.readlink(path))) path = target if target.is_absolute() else path.parent / target return path.resolve() +def link_target(text): + if os.name != "nt": + return text + if text.startswith("\\\\?\\UNC\\"): + return "\\\\" + text[8:] + if text.startswith("\\\\?\\"): + return text[4:] + return text + + def same_file(left, right): left, right = resolved_path(left), resolved_path(right) - if left == right: + if str(left) == str(right): return True if left.exists() and right.exists() and left.samefile(right): return True diff --git a/tools/tests/test_stubgen.py b/tools/tests/test_stubgen.py index 90475b13cc..435ae833f5 100644 --- a/tools/tests/test_stubgen.py +++ b/tools/tests/test_stubgen.py @@ -365,6 +365,14 @@ class CaseVariantOutputTests(unittest.TestCase): self.assertEqual(error.exception.code, 2) self.assertEqual(debt.read_text(encoding="utf-8"), original) + def test_windows_link_targets_drop_the_extended_length_prefix(self): + with patch.object(stubgen.os, "name", "nt"): + self.assertEqual(stubgen.link_target("\\\\?\\C:\\out\\REPORT.TXT"), "C:\\out\\REPORT.TXT") + self.assertEqual(stubgen.link_target("\\\\?\\UNC\\host\\share\\a.txt"), "\\\\host\\share\\a.txt") + self.assertEqual(stubgen.link_target("..\\REPORT.TXT"), "..\\REPORT.TXT") + with patch.object(stubgen.os, "name", "posix"): + self.assertEqual(stubgen.link_target("\\\\?\\C:\\x"), "\\\\?\\C:\\x") + def test_case_variant_outputs_stay_distinct_on_case_sensitive_directories(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) From d6b7da4cb6b2b866e2b3e0233d5934376180fd15 Mon Sep 17 00:00:00 2001 From: dantdmdarealguy Date: Wed, 28 Oct 2026 00:15:10 -0500 Subject: [PATCH 060/160] fix(opus): assemble libopus aligned vector moves as unaligned with MinGW GCC GCC on Windows x64 does not align the stack for 32-byte locals (GCC bug 54412) but still spills them with vmovdqa. The libopus AVX2 encoder code, selected at run time on AVX2 hosts, does this in Debug builds: guest_opus_silk_enc faults in silk_cvtepi64_epi32_high (silk/x86/NSQ_del_dec_avx2.c:89) at a 16-byte aligned stack address. Release builds pass only because these values stay in registers. -muse-unaligned-vector-move makes the assembler emit the unaligned forms of these moves, which accept any address and compute the same results. --- cmake/Opus.cmake | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmake/Opus.cmake b/cmake/Opus.cmake index d6bc3e3f78..3cef2a2680 100644 --- a/cmake/Opus.cmake +++ b/cmake/Opus.cmake @@ -11,6 +11,9 @@ function(add_bundled_opus) add_subdirectory(3rdparty/opus EXCLUDE_FROM_ALL) set_target_properties(opus PROPERTIES POSITION_INDEPENDENT_CODE ON C_VISIBILITY_PRESET hidden) target_compile_definitions(opus PRIVATE OPUS_EXPORT=) + if(MINGW AND CMAKE_C_COMPILER_ID STREQUAL "GNU") + target_compile_options(opus PRIVATE -Wa,-muse-unaligned-vector-move) + endif() if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(opus INTERFACE LINKER:--exclude-libs,libopus.a) endif() From 1200b5dd7baf5e2c83e07798d16d4efbebd6663e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= Date: Wed, 28 Oct 2026 12:23:40 +0100 Subject: [PATCH 061/160] fix(opus): feature-test the assembler option for unaligned vector moves Co-Authored-By: Claude Sonnet 5.5 --- cmake/Opus.cmake | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cmake/Opus.cmake b/cmake/Opus.cmake index 3cef2a2680..455604790f 100644 --- a/cmake/Opus.cmake +++ b/cmake/Opus.cmake @@ -12,7 +12,11 @@ function(add_bundled_opus) set_target_properties(opus PROPERTIES POSITION_INDEPENDENT_CODE ON C_VISIBILITY_PRESET hidden) target_compile_definitions(opus PRIVATE OPUS_EXPORT=) if(MINGW AND CMAKE_C_COMPILER_ID STREQUAL "GNU") - target_compile_options(opus PRIVATE -Wa,-muse-unaligned-vector-move) + include(CheckCCompilerFlag) + check_c_compiler_flag("-Wa,-muse-unaligned-vector-move" ANYPS5_ASM_UNALIGNED_VECTOR_MOVE) + if(ANYPS5_ASM_UNALIGNED_VECTOR_MOVE) + target_compile_options(opus PRIVATE -Wa,-muse-unaligned-vector-move) + endif() endif() if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(opus INTERFACE LINKER:--exclude-libs,libopus.a) From 3c79f76691926cd9a1e7e8d7a3d8cdeffcbed6d1 Mon Sep 17 00:00:00 2001 From: dantdmdarealguy Date: Wed, 28 Oct 2026 02:35:09 -0500 Subject: [PATCH 062/160] docs(contributing): describe the push relinker job and the manual run inputs --- CONTRIBUTING.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d96b4d4c11..5ec5dcf162 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,9 +32,9 @@ ctest --test-dir build --output-on-failure Python 3 is optional; without it some relinker tests are not registered. -Every push to `main`, including a merged pull request, automatically runs full Linux and Windows builds and tests and the relinker matrix on Linux, Windows and macOS, using the pushed commit without contributor checks. +Every push to `main`, including a merged pull request, automatically runs full Linux and Windows builds and tests, which include the relinker tests, using the pushed commit without contributor checks. The relinker-only build and tests run separately only on macOS, for arm64 and x86_64, and only when the push changes a file the relinker-only build uses. -For pull requests, full Linux and Windows builds and tests run only when a collaborator with write access selects Actions > Build > Run workflow on `main` and enters an open pull request number targeting `main`. A collaborator can start the same manual run from the command line, replacing `1234` with the pull request number: +For pull requests, full Linux and Windows builds and tests run only when a collaborator with write access selects Actions > Build > Run workflow on `main` and enters up to 40 numbers of open pull requests targeting `main`, separated by spaces or commas; they are merged in that order and built together. A collaborator can start the same manual run from the command line, replacing `1234` with the pull request numbers: ``` gh workflow run build.yml --repo boykopovar/AnyPS5 --ref main --field pr=1234 @@ -44,6 +44,8 @@ This manual run also builds and tests the relinker on Linux, Windows and macOS w With `--field full=false`, the manual run skips the full builds and runs only the relinker matrix on Linux, Windows and macOS, if the pull request changes a file the relinker-only build uses, such as the Python tools and their tests in `tools/`. +With `--field linux=false`, the manual run skips the full Linux build and adds Linux to the relinker matrix, if the pull requests change a file the relinker-only build uses. With `--field select=true`, the full builds build and run only the tests whose build inputs the pull requests change; push builds of `main` always run every test. + The Conventions check runs automatically for every contributor, including first-time contributors. It uses the checker from the current tip of the base branch, so rules added after a pull request was opened apply to its next run, and reads pull request Git objects without executing pull request code. The Conventions check runs on every pull request and fails when a rule on this page is broken. It accepts code comments only when the pull request also changes [technical debt](docs/dev/TechnicalDebt.md), the index or a file in `docs/dev/debt/`, and only UTF-8 text files. Run it locally before pushing: ``` From 2598b60aea9e8f6c7367bfb9bcb0cb20c5f3127a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= Date: Wed, 28 Oct 2026 12:27:38 +0100 Subject: [PATCH 063/160] docs(contributing): mention the Linux relinker job with the address sanitizer --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5ec5dcf162..61881a4db0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,7 +32,7 @@ ctest --test-dir build --output-on-failure Python 3 is optional; without it some relinker tests are not registered. -Every push to `main`, including a merged pull request, automatically runs full Linux and Windows builds and tests, which include the relinker tests, using the pushed commit without contributor checks. The relinker-only build and tests run separately only on macOS, for arm64 and x86_64, and only when the push changes a file the relinker-only build uses. +Every push to `main`, including a merged pull request, automatically runs full Linux and Windows builds and tests, which include the relinker tests, using the pushed commit without contributor checks. The relinker-only build and tests run separately on macOS, for arm64 and x86_64, and the relinker-only build with the address sanitizer runs on Linux, only when the push changes a file the relinker-only build uses. For pull requests, full Linux and Windows builds and tests run only when a collaborator with write access selects Actions > Build > Run workflow on `main` and enters up to 40 numbers of open pull requests targeting `main`, separated by spaces or commas; they are merged in that order and built together. A collaborator can start the same manual run from the command line, replacing `1234` with the pull request numbers: From b34ed97f6e5e40709b117504b7caa46bfde0ecfd Mon Sep 17 00:00:00 2001 From: StoneDC <19905433+StoneDC@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:48:01 +0800 Subject: [PATCH 064/160] fix(web): initialize readers before opening directory outputs --- demo/web/archive.mjs | 2 +- demo/web/tests/directory-writer-fixture.mjs | 58 +++++++++++++++++++++ demo/web/tests/directory-writer.test.mjs | 10 ++++ tools/test_web.py | 2 +- 4 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 demo/web/tests/directory-writer-fixture.mjs create mode 100644 demo/web/tests/directory-writer.test.mjs diff --git a/demo/web/archive.mjs b/demo/web/archive.mjs index 7021114f0e..7dd14ef357 100644 --- a/demo/web/archive.mjs +++ b/demo/web/archive.mjs @@ -281,6 +281,7 @@ export class DirectoryWriter { await this.destination.directory(name); return; } + if (!(source instanceof ReadableStream)) await source.init(); const output = await this.destination.file(name); if (source instanceof ReadableStream) { let completed = 0; @@ -306,7 +307,6 @@ export class DirectoryWriter { writer.releaseLock(); } } else { - await source.init(); const writer = output.getWriter(); try { for (let offset = 0; offset < source.size; offset += 65536) { diff --git a/demo/web/tests/directory-writer-fixture.mjs b/demo/web/tests/directory-writer-fixture.mjs new file mode 100644 index 0000000000..c4ddfc50f4 --- /dev/null +++ b/demo/web/tests/directory-writer-fixture.mjs @@ -0,0 +1,58 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import vm from "node:vm"; + +const context = vm.createContext({ Uint8Array, Blob, ReadableStream, WritableStream, TransformStream, AbortController, DataView }); +const module = new vm.SourceTextModule(readFileSync(new URL("../archive.mjs", import.meta.url), "utf8"), { context }); +await module.link(async name => { + const exports = name.startsWith("https:") ? { BlobReader: class {}, Uint8ArrayReader: class {}, ZipReader: class {}, ZipWriter: class {}, configure() {} } : { batchMemoryEstimate() {}, batchOutputDirectory() {}, memoryEstimate() {}, MEMORY_LIMIT: 0 }; + return new vm.SyntheticModule(Object.keys(exports), function() { for (const [key, value] of Object.entries(exports)) this.setExport(key, value); }, { context }); +}); +await module.evaluate(); +const { DirectoryWriter } = module.namespace; +let scenarios = 0; +let opened = 0; +await assert.rejects(new DirectoryWriter({ async file() { opened++; return new WritableStream(); } }).add("file", { async init() { throw new Error("init failed"); } }), /init failed/); +assert.equal(opened, 0); +scenarios++; +let directory = false; +await new DirectoryWriter({ async directory() { directory = true; }, async file() { throw new Error("unexpected file"); } }).add("dir/", null, { directory: true }); +assert.equal(directory, true); +scenarios++; +const order = []; +const chunks = []; +let closed = false; +const output = new WritableStream({ write(value) { chunks.push(new Uint8Array(value)); }, close() { closed = true; } }); +const source = { size: 150000, async init() { order.push("init"); }, async readUint8Array(offset, count) { assert.ok(count <= 65536); return Uint8Array.from({ length: count }, (_, index) => (offset + index) % 251); } }; +await new DirectoryWriter({ async file() { order.push("file"); return output; } }).add("file", source); +assert.deepEqual(order, ["init", "file"]); +assert.equal(Buffer.concat(chunks).length, 150000); +assert.equal(Buffer.concat(chunks)[149999], 149999 % 251); +assert.equal(closed, true); +assert.equal(output.locked, false); +scenarios++; +let aborted = false; +const brokenOutput = new WritableStream({ abort() { aborted = true; } }); +await assert.rejects(new DirectoryWriter({ async file() { return brokenOutput; } }).add("file", { size: 1, async init() {}, async readUint8Array() { throw new Error("read failed"); } }), /read failed/); +assert.equal(aborted, true); +assert.equal(brokenOutput.locked, false); +scenarios++; +const streaming = new ReadableStream({ start(controller) { controller.enqueue(new Uint8Array(150000)); controller.close(); } }); +let maximumWrite = 0; +let total = 0; +const streamed = new WritableStream({ write(value) { maximumWrite = Math.max(maximumWrite, value.length); total += value.length; } }); +await new DirectoryWriter({ async file() { return streamed; } }).add("file", streaming); +assert.equal(total, 150000); +assert.ok(maximumWrite <= 65536); +assert.equal(streaming.locked, false); +assert.equal(streamed.locked, false); +scenarios++; +let cancelled = false; +const pending = new ReadableStream({ pull(controller) { controller.enqueue(new Uint8Array([1])); }, cancel() { cancelled = true; } }); +const rejected = new WritableStream({ write() { throw new Error("write failed"); } }); +await assert.rejects(new DirectoryWriter({ async file() { return rejected; } }).add("file", pending), /write failed/); +assert.equal(cancelled, true); +assert.equal(pending.locked, false); +assert.equal(rejected.locked, false); +scenarios++; +console.log(JSON.stringify({ scenarios })); diff --git a/demo/web/tests/directory-writer.test.mjs b/demo/web/tests/directory-writer.test.mjs new file mode 100644 index 0000000000..473499d2c1 --- /dev/null +++ b/demo/web/tests/directory-writer.test.mjs @@ -0,0 +1,10 @@ +import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { fileURLToPath } from "node:url"; +import { test } from "node:test"; + +test("directory writer prepares its reader and cleans acquired stream resources", async () => { + const { stdout } = await promisify(execFile)(process.execPath, ["--experimental-vm-modules", fileURLToPath(new URL("./directory-writer-fixture.mjs", import.meta.url))], { timeout: 15000 }); + assert.ok(JSON.parse(stdout).scenarios >= 6); +}); diff --git a/tools/test_web.py b/tools/test_web.py index 6799fca777..aa174c8736 100644 --- a/tools/test_web.py +++ b/tools/test_web.py @@ -28,7 +28,7 @@ def main(argv=None): try: subprocess.run(["node", "--test", *(str(ROOT / "demo/web/tests" / name) for name in ( "app.test.mjs", "batch.test.mjs", "failure-report.test.mjs", "output-directory.test.mjs" - ))], check=True) + ) + ("directory-writer.test.mjs",))], check=True) with tempfile.TemporaryDirectory(prefix="anyps5-web-") as directory: fixture = Path(directory) (fixture / "main.elf").write_bytes(main_fixture()) From 0e9dba02ccdea1a4cd2dd58efabbe820ffb03a75 Mon Sep 17 00:00:00 2001 From: StoneDC <19905433+StoneDC@users.noreply.github.com> Date: Wed, 28 Oct 2026 19:21:44 +0800 Subject: [PATCH 065/160] test(web): run directory writer regressions in pull request CI --- .github/workflows/web-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml index cd39336b6f..5605e3a0c4 100644 --- a/.github/workflows/web-tests.yml +++ b/.github/workflows/web-tests.yml @@ -38,3 +38,5 @@ jobs: node --check demo/web/worker.mjs - name: Test browser converter helpers run: node --test demo/web/tests/app.test.mjs demo/web/tests/batch.test.mjs demo/web/tests/failure-report.test.mjs demo/web/tests/output-directory.test.mjs + - name: Test directory writer resources + run: node --test demo/web/tests/directory-writer.test.mjs From def921411987926b6000ccd93165def1c36326e5 Mon Sep 17 00:00:00 2001 From: Alexandre Figueiredo Date: Sun, 25 Oct 2026 23:35:53 +0000 Subject: [PATCH 066/160] fix(libc): return the sign mask from __signbit, __signbitf and __signbitl --- core/libs/prx/libc/src/Math.cpp | 6 +++--- core/libs/tests/GuestMath.cpp | 12 ++++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 9c34d18ed7..de2d026e42 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -1715,7 +1715,7 @@ double APS5_VABI nextafter_nid_postfix(double x, double y) { int APS5_VABI __isfinite_nid_postfix(double x) { return std::isfinite(x) ? 1 : 0; } int APS5_VABI __isnan_nid_postfix(double x) { return std::isnan(x) ? 1 : 0; } int APS5_VABI __isinf_nid_postfix(double x) { return std::isinf(x) ? 1 : 0; } -int APS5_VABI __signbit_nid_postfix(double x) { return std::signbit(x) ? 1 : 0; } +int APS5_VABI __signbit_nid_postfix(double x) { return std::signbit(x) ? 0x8000 : 0; } float APS5_VABI nanf_nid_postfix(const char* tagp) { (void)tagp; return std::bit_cast(0x7fc00000u); @@ -1867,10 +1867,10 @@ int APS5_VABI isnanf_nid_postfix(float x) { return __isnanf_nid_postfix(x); } int APS5_VABI isnan_nid_postfix(double x) { return __isnan_nid_postfix(x); } int APS5_VABI isinf_nid_postfix(double x) { return __isinf_nid_postfix(x); } int APS5_VABI __fpclassifyf_nid_postfix(float x) { return _FDtest_nid_postfix(&x); } -int APS5_VABI __signbitf_nid_postfix(float x) { return std::signbit(x) ? 1 : 0; } +int APS5_VABI __signbitf_nid_postfix(float x) { return std::signbit(x) ? 0x8000 : 0; } int APS5_VABI __signbitl_nid_postfix(long double x) { static_assert(sizeof(long double) == 16, "guest long double requires x87 extended precision storage"); - return std::signbit(x) ? 1 : 0; + return std::signbit(x) ? 0x8000 : 0; } int APS5_VABI __isfinitel_nid_postfix(long double x) { diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 80f1217e86..e3245fedb6 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -1827,11 +1827,15 @@ static void CheckMissingClassificationExports() { Require(isnanf_nid_postfix(0.f) == 0 && isnanf_nid_postfix(-1.f) == 0); const long double negativeNan = std::copysign(std::numeric_limits::quiet_NaN(), -1.L); - Require(__signbitl_nid_postfix(0.L) == 0 && __signbitl_nid_postfix(-0.L) == 1); - Require(__signbitl_nid_postfix(1.L) == 0 && __signbitl_nid_postfix(-1.L) == 1); + Require(__signbitl_nid_postfix(0.L) == 0 && __signbitl_nid_postfix(-0.L) == 0x8000); + Require(__signbitl_nid_postfix(1.L) == 0 && __signbitl_nid_postfix(-1.L) == 0x8000); Require(__signbitl_nid_postfix(std::numeric_limits::infinity()) == 0); - Require(__signbitl_nid_postfix(-std::numeric_limits::infinity()) == 1); - Require(__signbitl_nid_postfix(negativeNan) == 1); + Require(__signbitl_nid_postfix(-std::numeric_limits::infinity()) == 0x8000); + Require(__signbitl_nid_postfix(negativeNan) == 0x8000); + Require(__signbit_nid_postfix(0.0) == 0 && __signbit_nid_postfix(-0.0) == 0x8000 && __signbit_nid_postfix(-1e-310) == 0x8000); + Require(__signbit_nid_postfix(std::copysign(std::numeric_limits::quiet_NaN(), -1.0)) == 0x8000); + Require(__signbitf_nid_postfix(1.f) == 0 && __signbitf_nid_postfix(-0.f) == 0x8000 && __signbitf_nid_postfix(-infinity) == 0x8000); + Require(__signbitf_nid_postfix(nan) == 0 && __signbitf_nid_postfix(std::copysign(nan, -1.f)) == 0x8000); } static void CheckFloatMathExports() { From 410e3b8b21f74c58834698a08918d48c05fb33dd Mon Sep 17 00:00:00 2001 From: Misjudgments Date: Tue, 13 Oct 2026 15:49:13 +0300 Subject: [PATCH 067/160] fix(libSceFont): keep horizontal writing on a single baseline Horizontal writing steps advanced the pen by the glyph's vertical advance on every character, so text drifted down diagonally (steepest for CJK fonts, whose vertical advance is roughly the em size). The step's GlyphMetrics width/height were also overwritten with the pen position, leaving the first glyph of each run invisible at pen (0, 0). Every supported writing form (0x10/0x11/0x12) is horizontal, so the pen must advance only along X and the step reports the real glyph width/height. --- core/libs/prx/libSceFont/src/Text.cpp | 4 +--- docs/dev/debt/libSceFont.md | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceFont/src/Text.cpp b/core/libs/prx/libSceFont/src/Text.cpp index 21c7596e18..cb3244f518 100644 --- a/core/libs/prx/libSceFont/src/Text.cpp +++ b/core/libs/prx/libSceFont/src/Text.cpp @@ -165,12 +165,10 @@ void FillWritingMetricsFromGlyph(FontWritingData& writing, const FontGlyphMetric writing.rawGlyphMetrics = glyphMetrics; writing.hasRawGlyphMetrics = true; writing.step.advanceX = glyphMetrics.Horizontal.advance; - writing.step.advanceY = glyphMetrics.Vertical.advance; + writing.step.advanceY = 0.0f; writing.step.GlyphMetrics = glyphMetrics; writing.step.Positioning.x = writing.step.x; writing.step.Positioning.y = writing.step.y; - writing.step.GlyphMetrics.width = writing.step.x; - writing.step.GlyphMetrics.height = writing.step.y; } bool PopulateWritingStep(FontWritingData& writing, std::size_t index) { diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index a0f4c548a1..d6350784ff 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -51,3 +51,4 @@ The [glyph support registration and texture-image fill signatures](../../../core - [sceFontCreateGraphicsServiceWithEdition and sceFontCreateWords](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) take their handle output in the fourth integer argument slot. The third slot is not read by the supplied module and is represented as an opaque 64-bit value; its declared source type remains unknown. - [Word dictionaries](../../../core/libs/prx/libSceFont/src/Words.cpp): building, depth limit, sort and error rules and longest-match selection are read from disassembly, not measured on a console. The text source is parsed once instead of twice. More than 65535 nodes return `SCE_FONT_ERROR_FUNCTIONAL_LIMIT`, where the native 16-bit indices would wrap. The shape-entry test on the next character is skipped since strings built here have no shape entry. The low flag byte and flag bits 43-45 are tested as read; strings built here leave them clear, so names are ours. +- [Font writing steps](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - every supported writing form (`0x10`, `0x11`, `0x12`) is horizontal, so a writing step's `advanceY` and the pen's vertical movement are zero and the step's `GlyphMetrics` keep the glyph's own width and height. This follows steps measured on PS4 11.00 firmware ("Ab c": `y` stays 0, `advanceY` is 0 on every step), not shadPS4's `FillWritingMetricsFromGlyph`, which copies the vertical advance. The drift came from advancing the pen by the vertical advance, not from the value of that advance: it stays reported by `sceFontGetCharGlyphMetrics` and its FreeType source is still unconfirmed on a title (see the metrics entry above). The step's width/height are not measured on the PS4 firmware; they keep the glyph's values. From 35f854562ed6a03b4514ec796c11c5716094f3e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= Date: Wed, 28 Oct 2026 12:51:02 +0100 Subject: [PATCH 068/160] test(libSceFont): pin horizontal writing steps on a single baseline Test adapted from JAB-dev's comment on #5209. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestSystemFont.cpp | 41 +++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/core/libs/tests/GuestSystemFont.cpp b/core/libs/tests/GuestSystemFont.cpp index f4b5946c3c..c38a2bbae3 100644 --- a/core/libs/tests/GuestSystemFont.cpp +++ b/core/libs/tests/GuestSystemFont.cpp @@ -290,6 +290,24 @@ static void CheckGlyphImages(FontHandle font, FontRenderer renderer) { Require(sceFontDeleteGlyph(nullptr, &glyph) == SCE_FONT_OK && glyph == nullptr); } +struct ParsedText { + const char* text; + std::size_t at; + FontHandle font; +}; + +static std::int32_t APS5_VABI ParseText(FontTextSource* source, void** order, FontTextParseResult* result) { + auto* parsed = static_cast(source->textObject); + *order = const_cast(parsed->text + parsed->at); + if (parsed->text[parsed->at] == 0) { + result->Terminate.terminateCode = 0; + return 0; + } + result->FontCode.font = parsed->font; + result->FontCode.code = static_cast(parsed->text[parsed->at++]); + return 1; +} + int main() { constexpr std::uint32_t EuropeanLight = 0x180700C3u; constexpr std::uint32_t EuropeanBold = 0x180700C7u; @@ -352,6 +370,29 @@ int main() { FontHorizontalLayout layout{}; Require(sceFontGetHorizontalLayout(font, &layout) == SCE_FONT_OK && layout.baselineOffset > 0.0f && layout.lineAdvance >= layout.baselineOffset); + ParsedText parsed{"AA", 0, font}; + FontTextSource source{}; + Require(sceFontTextSourceInit(&source, parsed.text, 2, ParseText, &parsed) == SCE_FONT_OK); + FontString string = nullptr; + Require(sceFontCreateString(&memory, &source, nullptr, &string) == SCE_FONT_OK && string != nullptr); + std::uint32_t characterCount = 0; + FontTextCharacter* characters = sceFontStringRefersTextCharacters(string, &characterCount); + Require(characters != nullptr && characterCount == 2); + FontWriting writing{}; + Require(sceFontWritingInit(&writing, string, characters) == SCE_FONT_OK); + const FontWritingStep* step = sceFontWritingRefersRenderStep(&writing); + Require(step != nullptr); + const FontWritingStep first = *step; + step = sceFontWritingRefersRenderStep(&writing); + Require(step != nullptr); + const FontWritingStep second = *step; + Require(first.x == 0.0f && first.y == 0.0f && first.advanceX == 70.0f && first.advanceY == 0.0f); + Require(second.x == 70.0f && second.y == 0.0f && second.advanceX == 70.0f && second.advanceY == 0.0f); + Require(first.GlyphMetrics.width == 50.0f && first.GlyphMetrics.height == 70.0f); + Require(second.GlyphMetrics.width == 50.0f && second.GlyphMetrics.height == 70.0f); + Require(second.GlyphMetrics.Vertical.advance == 100.0f); + Require(sceFontDestroyString(&string) == SCE_FONT_OK); + FontHandle again = nullptr; Require(sceFontOpenFontSet(library, EuropeanBold, 2, nullptr, &again) == SCE_FONT_OK && again != font); FontHandle instance = nullptr; From d5ed79e78a9260e6cb2131e32d5e80320492038b Mon Sep 17 00:00:00 2001 From: rooklz <175737347+rooklz@users.noreply.github.com> Date: Wed, 28 Oct 2026 11:49:14 +0000 Subject: [PATCH 069/160] fix(tools): report symbolic link loops in progress_history as usage errors --- tools/progress_history.py | 8 ++++++-- tools/tests/test_progress_history.py | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/tools/progress_history.py b/tools/progress_history.py index 6a74f0f41b..6f3452ae58 100644 --- a/tools/progress_history.py +++ b/tools/progress_history.py @@ -226,6 +226,10 @@ def page(points): ]) + "\n" +def same_location(first, second): + return os.path.normcase(os.path.realpath(first)) == os.path.normcase(os.path.realpath(second)) + + def main(argv=None): parser = argparse.ArgumentParser() parser.add_argument("output", type=Path) @@ -236,10 +240,10 @@ def main(argv=None): try: for index, path in enumerate(outputs): for other in outputs[index + 1:]: - if (path.exists() and other.exists() and path.samefile(other)) or path.resolve() == other.resolve(): + if (path.exists() and other.exists() and path.samefile(other)) or same_location(path, other): raise ValueError(f"generated reports cannot alias each other: {path.name} and {other.name}") if args.cache: - if any(args.cache.resolve() == path.resolve() or + if any(same_location(args.cache, path) or (args.cache.exists() and path.exists() and args.cache.samefile(path)) for path in outputs): raise ValueError("history cache must differ from generated reports") points = collect(args.ref, args.cache) diff --git a/tools/tests/test_progress_history.py b/tools/tests/test_progress_history.py index 4f4e8f296c..f8924e3c90 100644 --- a/tools/tests/test_progress_history.py +++ b/tools/tests/test_progress_history.py @@ -229,6 +229,32 @@ class ProgressHistoryTests(unittest.TestCase): self.assertEqual(report.read_text(encoding="utf-8"), "existing json") self.assertEqual(alias.read_text(encoding="utf-8"), "existing json") + def test_main_symbolic_link_loops_do_not_escape_as_tracebacks(self): + point = {"date": "2026-01-01", "commit": "abc1234", + "libraries": {"done": 1, "total": 2, "percent": 50.0}, + "shaders": {"done": 3, "total": 4, "percent": 75.0}} + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + loop = root / "loop" + try: + os.symlink(loop, loop) + except OSError: + self.skipTest("symbolic links unavailable") + with self.subTest("cache"): + output = root / "site" + with patch.object(progress_history, "collect", return_value=[point]) as collect: + with contextlib.redirect_stdout(io.StringIO()): + progress_history.main([str(output), "--cache", str(loop)]) + collect.assert_called_once_with("HEAD", loop) + self.assertEqual(json.loads((output / "history.json").read_text(encoding="utf-8")), [point]) + with self.subTest("output"): + errors = io.StringIO() + with patch.object(progress_history, "collect", return_value=[point]): + with contextlib.redirect_stderr(errors), self.assertRaises(SystemExit) as error: + progress_history.main([str(loop), "--cache", str(root / "counts.json")]) + self.assertEqual(error.exception.code, 2) + self.assertIn(str(loop), errors.getvalue()) + if __name__ == "__main__": From 0742c4f6b4e1b309bb0a2ccad4a0246fab1f5279 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 21 Oct 2026 18:22:17 +0100 Subject: [PATCH 070/160] feat(libkernel): let other libraries observe closed guest descriptors --- core/libs/prx/libkernel/Equeue/Equeue.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core/libs/prx/libkernel/Equeue/Equeue.cpp b/core/libs/prx/libkernel/Equeue/Equeue.cpp index c3d1691407..367ec4098b 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.cpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.cpp @@ -910,8 +910,26 @@ KernelEqueueEvent EqueueDescriptorEvent(int descriptor, int16_t filter, uint16_t return event; } +static std::mutex g_closeObserverMutex; +static std::vector g_closeObservers; + +extern "C" void RegisterDescriptorCloseObserver_nid_no_patch(void (*observer)(int descriptor)) { + std::lock_guard lock(g_closeObserverMutex); + g_closeObservers.push_back(observer); +} + +static void NotifyDescriptorCloseObservers(int descriptor) { + std::vector observers; + { + std::lock_guard lock(g_closeObserverMutex); + observers = g_closeObservers; + } + for (const auto observer : observers) observer(descriptor); +} + void EqueueDescriptorClosed(int descriptor) { KqueueDescriptorClosed(descriptor); + NotifyDescriptorCloseObservers(descriptor); std::vector owners; { std::unique_lock lock(g_equeueMutex); From c2939224e8eb8ea1ec3f13c6cd0a23c467607987 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 21 Oct 2026 18:22:17 +0100 Subject: [PATCH 071/160] feat(libkernel_sys): implement the unnamed ksem semaphores --- core/libs/prx/libkernel_sys/CMakeLists.txt | 1 + .../prx/libkernel_sys/Export.Discovery.cpp | 35 ---- core/libs/prx/libkernel_sys/Ksem.cpp | 163 ++++++++++++++++++ 3 files changed, 164 insertions(+), 35 deletions(-) create mode 100644 core/libs/prx/libkernel_sys/Ksem.cpp diff --git a/core/libs/prx/libkernel_sys/CMakeLists.txt b/core/libs/prx/libkernel_sys/CMakeLists.txt index 70a25df4cd..c35b590dae 100644 --- a/core/libs/prx/libkernel_sys/CMakeLists.txt +++ b/core/libs/prx/libkernel_sys/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(${LIB_NAME} SHARED EXCLUDE_FROM_ALL Export.cpp Export.Discovery.cpp Aio.cpp Cpuset.cpp ThreadQueries.cpp) target_include_directories(${LIB_NAME} PRIVATE ${LIBS_INCLUDE_DIR}) +target_sources(${LIB_NAME} PRIVATE Ksem.cpp) configure_windows_unwind(${LIB_NAME}) target_sources(${LIB_NAME} PRIVATE Uuid.cpp) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index fbdef209d4..531a96f217 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -155,51 +155,16 @@ int APS5_VABI ksem_close_nid_postfix(void) { return 0; } -int APS5_VABI ksem_destroy_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ksem_getvalue_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ksem_init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI ksem_open_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI ksem_post_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ksem_timedwait_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ksem_trywait_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI ksem_unlink_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI ksem_wait_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI ktrace_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel_sys/Ksem.cpp b/core/libs/prx/libkernel_sys/Ksem.cpp new file mode 100644 index 0000000000..57910db95d --- /dev/null +++ b/core/libs/prx/libkernel_sys/Ksem.cpp @@ -0,0 +1,163 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#else +#include +#include +#endif + +extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI close_nid_postfix(int descriptor); +extern "C" void RegisterDescriptorCloseObserver_nid_no_patch(void (*observer)(int descriptor)); + +namespace { + +constexpr int GuestEfault = 14; +constexpr int GuestEbusy = 16; +constexpr int GuestEinval = 22; +constexpr int GuestEnfile = 23; +constexpr int GuestEmfile = 24; +constexpr int GuestEagain = 35; +constexpr int GuestEtimedout = 60; +constexpr int GuestEoverflow = 84; +constexpr unsigned SemValueMax = 0x7fffffffu; + +struct GuestTimespec { + std::int64_t seconds; + std::int64_t nanoseconds; +}; + +struct Ksem { + std::mutex lock; + std::condition_variable available; + unsigned count = 0; + unsigned waiters = 0; +}; + +std::mutex g_ksemLock; +std::map> g_ksems; + +int Fail(int error) { + *__error_nid_postfix() = error; + return -1; +} + +std::shared_ptr Find(std::intptr_t id) { + if (id < 0 || id > INT_MAX) return {}; + std::lock_guard lock(g_ksemLock); + const auto found = g_ksems.find(static_cast(id)); + return found != g_ksems.end() ? found->second : std::shared_ptr{}; +} + +int ReserveDescriptor() { +#ifdef _WIN32 + return ::_open("NUL", _O_RDONLY | _O_BINARY); +#else + return ::open("/dev/null", O_RDONLY | O_CLOEXEC); +#endif +} + +void DescriptorClosed(int descriptor) { + std::lock_guard lock(g_ksemLock); + g_ksems.erase(descriptor); +} + +const bool g_observerRegistered = [] { + RegisterDescriptorCloseObserver_nid_no_patch(DescriptorClosed); + return true; +}(); + +int Wait(std::intptr_t id, bool once, const GuestTimespec* abstime) { + const auto ksem = Find(id); + if (!ksem) return Fail(GuestEinval); + std::unique_lock lock(ksem->lock); + if (ksem->count == 0) { + if (once) return Fail(GuestEagain); + ++ksem->waiters; + bool ready = true; + if (abstime == nullptr) { + ksem->available.wait(lock, [&] { return ksem->count > 0; }); + } else { + const auto deadline = std::chrono::system_clock::time_point(std::chrono::duration_cast( + std::chrono::seconds(abstime->seconds) + std::chrono::nanoseconds(abstime->nanoseconds))); + ready = ksem->available.wait_until(lock, deadline, [&] { return ksem->count > 0; }); + } + --ksem->waiters; + if (!ready) return Fail(GuestEtimedout); + } + --ksem->count; + return 0; +} + +} + +extern "C" { + +int APS5_VABI ksem_init_nid_postfix(std::intptr_t* idp, unsigned int value) { + if (value > SemValueMax) return Fail(GuestEinval); + if (idp == nullptr) return Fail(GuestEfault); + const int descriptor = ReserveDescriptor(); + if (descriptor < 0) return Fail(errno == ENFILE ? GuestEnfile : GuestEmfile); + auto ksem = std::make_shared(); + ksem->count = value; + { + std::lock_guard lock(g_ksemLock); + g_ksems[descriptor] = std::move(ksem); + } + *idp = descriptor; + return 0; +} + +int APS5_VABI ksem_post_nid_postfix(std::intptr_t id) { + const auto ksem = Find(id); + if (!ksem) return Fail(GuestEinval); + std::lock_guard lock(ksem->lock); + if (ksem->count == SemValueMax) return Fail(GuestEoverflow); + ++ksem->count; + ksem->available.notify_one(); + return 0; +} + +int APS5_VABI ksem_wait_nid_postfix(std::intptr_t id) { + return Wait(id, false, nullptr); +} + +int APS5_VABI ksem_trywait_nid_postfix(std::intptr_t id) { + return Wait(id, true, nullptr); +} + +int APS5_VABI ksem_timedwait_nid_postfix(std::intptr_t id, const GuestTimespec* abstime) { + if (abstime != nullptr && (abstime->nanoseconds < 0 || abstime->nanoseconds >= 1000000000)) return Fail(GuestEinval); + return Wait(id, false, abstime); +} + +int APS5_VABI ksem_getvalue_nid_postfix(std::intptr_t id, int* value) { + const auto ksem = Find(id); + if (!ksem) return Fail(GuestEinval); + if (value == nullptr) return Fail(GuestEfault); + std::lock_guard lock(ksem->lock); + *value = static_cast(ksem->count); + return 0; +} + +int APS5_VABI ksem_destroy_nid_postfix(std::intptr_t id) { + const auto ksem = Find(id); + if (!ksem) return Fail(GuestEinval); + { + std::lock_guard lock(ksem->lock); + if (ksem->waiters != 0) return Fail(GuestEbusy); + } + return close_nid_postfix(static_cast(id)); +} + +} From 0c1efe7bae81c9531f6f4dc0a229fa281a9a92b9 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 21 Oct 2026 18:22:17 +0100 Subject: [PATCH 072/160] test(libkernel_sys): cover the unnamed ksem semaphores, including closed and replaced ids --- core/libs/CMakeLists.txt | 11 ++ core/libs/tests/GuestKernelSysKsem.cpp | 146 +++++++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 core/libs/tests/GuestKernelSysKsem.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index dc463eea7f..f1aa08fd0e 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -827,6 +827,17 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_kernel_sys_ksem_tests tests/GuestKernelSysKsem.cpp) + target_include_directories(guest_kernel_sys_ksem_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_ksem_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_ksem_tests) + add_test(NAME guest_kernel_sys_ksem COMMAND guest_kernel_sys_ksem_tests) + set_tests_properties(guest_kernel_sys_ksem PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_kernel_sys_ksem PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_system_gesture_tests tests/GuestSystemGesture.cpp) target_include_directories(guest_system_gesture_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_system_gesture_tests PRIVATE libSceSystemGesture libc) diff --git a/core/libs/tests/GuestKernelSysKsem.cpp b/core/libs/tests/GuestKernelSysKsem.cpp new file mode 100644 index 0000000000..8753686a36 --- /dev/null +++ b/core/libs/tests/GuestKernelSysKsem.cpp @@ -0,0 +1,146 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI open_nid_postfix(const char* path, int flags, int mode); +int APS5_VABI close_nid_postfix(int descriptor); +int APS5_VABI dup2_nid_postfix(int from, int to); +int APS5_VABI ksem_init_nid_postfix(std::intptr_t* idp, unsigned int value); +int APS5_VABI ksem_post_nid_postfix(std::intptr_t id); +int APS5_VABI ksem_wait_nid_postfix(std::intptr_t id); +int APS5_VABI ksem_trywait_nid_postfix(std::intptr_t id); +int APS5_VABI ksem_timedwait_nid_postfix(std::intptr_t id, const void* abstime); +int APS5_VABI ksem_getvalue_nid_postfix(std::intptr_t id, int* value); +int APS5_VABI ksem_destroy_nid_postfix(std::intptr_t id); +} + +namespace { + +constexpr int Efault = 14; +constexpr int Ebusy = 16; +constexpr int Einval = 22; +constexpr int Eagain = 35; +constexpr int Etimedout = 60; +constexpr int Eoverflow = 84; + +struct Timespec { + std::int64_t seconds; + std::int64_t nanoseconds; +}; + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "check failed at line %u\n", static_cast(location.line())); + std::exit(1); +} + +int Error() { + return *__error_nid_postfix(); +} + +Timespec FromNow(std::chrono::milliseconds delay) { + const auto when = std::chrono::system_clock::now() + delay; + const auto nanoseconds = std::chrono::duration_cast(when.time_since_epoch()).count(); + return {nanoseconds / 1000000000, nanoseconds % 1000000000}; +} + +int Value(std::intptr_t id) { + int value = -1; + Require(ksem_getvalue_nid_postfix(id, &value) == 0); + return value; +} + +void Counting() { + std::intptr_t id = -1; + Require(ksem_init_nid_postfix(&id, 0x80000000u) == -1 && Error() == Einval); + Require(ksem_init_nid_postfix(nullptr, 1) == -1 && Error() == Efault); + Require(ksem_init_nid_postfix(&id, 2) == 0 && id >= 0); + Require(Value(id) == 2); + Require(ksem_trywait_nid_postfix(id) == 0 && ksem_wait_nid_postfix(id) == 0 && Value(id) == 0); + Require(ksem_trywait_nid_postfix(id) == -1 && Error() == Eagain); + Require(ksem_post_nid_postfix(id) == 0 && Value(id) == 1); + Require(ksem_getvalue_nid_postfix(id, nullptr) == -1 && Error() == Efault); + Require(ksem_destroy_nid_postfix(id) == 0); + Require(ksem_post_nid_postfix(id) == -1 && Error() == Einval); + Require(ksem_init_nid_postfix(&id, 0x7fffffffu) == 0); + Require(ksem_post_nid_postfix(id) == -1 && Error() == Eoverflow); + Require(ksem_destroy_nid_postfix(id) == 0); +} + +void Timed() { + std::intptr_t id = -1; + Require(ksem_init_nid_postfix(&id, 0) == 0); + const Timespec invalid{0, 1000000000}; + Require(ksem_timedwait_nid_postfix(id, &invalid) == -1 && Error() == Einval); + const auto deadline = FromNow(std::chrono::milliseconds(150)); + const auto start = std::chrono::steady_clock::now(); + Require(ksem_timedwait_nid_postfix(id, &deadline) == -1 && Error() == Etimedout); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(100)); + Require(ksem_post_nid_postfix(id) == 0); + const auto later = FromNow(std::chrono::seconds(5)); + Require(ksem_timedwait_nid_postfix(id, &later) == 0 && Value(id) == 0); + Require(ksem_destroy_nid_postfix(id) == 0); +} + +void Blocking() { + std::intptr_t id = -1; + Require(ksem_init_nid_postfix(&id, 0) == 0); + std::atomic result{-2}; + std::thread waiter([&] { result = ksem_wait_nid_postfix(id); }); + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + Require(result == -2); + Require(ksem_destroy_nid_postfix(id) == -1 && Error() == Ebusy); + Require(ksem_post_nid_postfix(id) == 0); + waiter.join(); + Require(result == 0 && Value(id) == 0); + Require(ksem_destroy_nid_postfix(id) == 0); +} + +void Identifiers() { + Require(ksem_post_nid_postfix(-1) == -1 && Error() == Einval); + const auto path = (std::filesystem::temp_directory_path() / "anyps5-ksem-stale.bin").string(); + { std::ofstream stream(path); stream << "x"; } + const int plain = open_nid_postfix(path.c_str(), 0, 0); + Require(plain >= 0); + Require(ksem_post_nid_postfix(plain) == -1 && Error() == Einval); + Require(close_nid_postfix(plain) == 0); + std::intptr_t id = -1; + Require(ksem_init_nid_postfix(&id, 1) == 0); + Require(close_nid_postfix(static_cast(id)) == 0); + Require(ksem_post_nid_postfix(id) == -1 && Error() == Einval); + const int reused = open_nid_postfix(path.c_str(), 0, 0); + Require(reused >= 0); + Require(ksem_post_nid_postfix(reused) == -1 && Error() == Einval); + Require(ksem_getvalue_nid_postfix(reused, nullptr) == -1 && Error() == Einval); + Require(close_nid_postfix(reused) == 0); + Require(ksem_init_nid_postfix(&id, 1) == 0); + const int other = open_nid_postfix(path.c_str(), 0, 0); + Require(other >= 0 && other != id); + Require(dup2_nid_postfix(other, static_cast(id)) == static_cast(id)); + Require(ksem_post_nid_postfix(id) == -1 && Error() == Einval); + Require(ksem_trywait_nid_postfix(id) == -1 && Error() == Einval); + Require(close_nid_postfix(static_cast(id)) == 0 && close_nid_postfix(other) == 0); + std::filesystem::remove(path); +} + +} + +int main() { + Counting(); + Timed(); + Blocking(); + Identifiers(); + std::puts("ksem unnamed semaphores passed"); + return 0; +} From 641df21cfd9ed4682a2e1e81881c1e365b6c5db3 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 21 Oct 2026 21:41:57 +0100 Subject: [PATCH 073/160] test(libkernel_sys): open the ksem stale-descriptor file by a guest-relative name An absolute host temp path is not mapped by the Linux guest resolver. Found by xXPBSXx on #9097. --- core/libs/tests/GuestKernelSysKsem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/tests/GuestKernelSysKsem.cpp b/core/libs/tests/GuestKernelSysKsem.cpp index 8753686a36..7a544d00c6 100644 --- a/core/libs/tests/GuestKernelSysKsem.cpp +++ b/core/libs/tests/GuestKernelSysKsem.cpp @@ -109,7 +109,7 @@ void Blocking() { void Identifiers() { Require(ksem_post_nid_postfix(-1) == -1 && Error() == Einval); - const auto path = (std::filesystem::temp_directory_path() / "anyps5-ksem-stale.bin").string(); + const auto path = "anyps5-ksem-stale-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".bin"; { std::ofstream stream(path); stream << "x"; } const int plain = open_nid_postfix(path.c_str(), 0, 0); Require(plain >= 0); From fd6513d6ff5e7e548e73d4eb7f9be01b8144958c Mon Sep 17 00:00:00 2001 From: David Date: Wed, 21 Oct 2026 21:46:21 +0100 Subject: [PATCH 074/160] refactor(libkernel): move the unnamed ksem calls into libkernel The PS5 3.20 export lists have ksem_* in libkernel as well as libkernel_sys, and on Windows a libkernel import never reaches libkernel_sys, so titles importing them from libkernel did not find them. Following pipe1os's review on #9284, also: ENOSPC from ksem_init when no descriptor is left, the SEM_MAX limit of 30 with ENOSPC, and dup/dup2 from a ksem id throwing as for a kqueue. --- core/libs/prx/libkernel/CMakeLists.txt | 1 + core/libs/prx/libkernel/File/src/Stdio.cpp | 3 ++ .../prx/libkernel/Semaphore/include/Ksem.hpp | 6 +++ .../Semaphore/src}/Ksem.cpp | 28 ++++++++++---- core/libs/prx/libkernel_sys/CMakeLists.txt | 1 - core/libs/tests/GuestKernelSysKsem.cpp | 38 +++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 7 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 core/libs/prx/libkernel/Semaphore/include/Ksem.hpp rename core/libs/prx/{libkernel_sys => libkernel/Semaphore/src}/Ksem.cpp (88%) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 2a70b082f2..801b0165ec 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -20,6 +20,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/File/src/SandboxCertificates.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/Stdio.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../libc/src/Rename.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Semaphore/src/Ksem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Semaphore/src/Semaphore.cpp ${CMAKE_CURRENT_SOURCE_DIR}/SyncOnAddress/src/SyncOnAddress.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DirectMemory/Export.cpp diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index b94988ef1f..ea142daa3c 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -24,6 +24,7 @@ #include "prx/libc/include/AppArchive.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" +#include "prx/libkernel/Semaphore/include/Ksem.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" #include @@ -849,6 +850,7 @@ int APS5_VABI dup_nid_postfix(int d) { if (GuestSockets::IsOpen(d)) return GuestSockets::Duplicate(d); if (d < 0) return PosixFailure(GUEST_EBADF); if (IsKqueueDescriptor_nid_no_patch(d)) throw std::runtime_error(std::string(__func__) + ": duplicating a kqueue descriptor is not supported"); + if (IsKsemDescriptor(d)) throw std::runtime_error(std::string(__func__) + ": duplicating a ksem id is not supported"); #ifdef _WIN32 const int duplicate = WithoutParameterHandler([d] { return ::_dup(d); }); if (duplicate >= 0) File::DuplicateSharedMemoryDescriptor_nid_no_patch(d, duplicate); @@ -880,6 +882,7 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { } const bool sourceIsRandom = File::IsRandomDevice(from); if (from != to && IsKqueueDescriptor_nid_no_patch(from)) throw std::runtime_error(std::string(__func__) + ": duplicating a kqueue descriptor is not supported"); + if (from != to && IsKsemDescriptor(from)) throw std::runtime_error(std::string(__func__) + ": duplicating a ksem id is not supported"); #ifdef _WIN32 if (WithoutParameterHandler([from] { return ::_get_osfhandle(from) == -1 ? -1 : 0; }) != 0) return PosixFailure(GUEST_EBADF); if (from == to) return to; diff --git a/core/libs/prx/libkernel/Semaphore/include/Ksem.hpp b/core/libs/prx/libkernel/Semaphore/include/Ksem.hpp new file mode 100644 index 0000000000..55d99485ae --- /dev/null +++ b/core/libs/prx/libkernel/Semaphore/include/Ksem.hpp @@ -0,0 +1,6 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_SEMAPHORE_KSEM_HPP +#define CORE_LIBS_PRX_LIBKERNEL_SEMAPHORE_KSEM_HPP + +bool IsKsemDescriptor(int descriptor); + +#endif diff --git a/core/libs/prx/libkernel_sys/Ksem.cpp b/core/libs/prx/libkernel/Semaphore/src/Ksem.cpp similarity index 88% rename from core/libs/prx/libkernel_sys/Ksem.cpp rename to core/libs/prx/libkernel/Semaphore/src/Ksem.cpp index 57910db95d..743b919295 100644 --- a/core/libs/prx/libkernel_sys/Ksem.cpp +++ b/core/libs/prx/libkernel/Semaphore/src/Ksem.cpp @@ -1,6 +1,6 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -#include +#include "prx/libkernel/Semaphore/include/Ksem.hpp" #include #include #include @@ -25,12 +25,12 @@ namespace { constexpr int GuestEfault = 14; constexpr int GuestEbusy = 16; constexpr int GuestEinval = 22; -constexpr int GuestEnfile = 23; -constexpr int GuestEmfile = 24; +constexpr int GuestEnospc = 28; constexpr int GuestEagain = 35; constexpr int GuestEtimedout = 60; constexpr int GuestEoverflow = 84; constexpr unsigned SemValueMax = 0x7fffffffu; +constexpr std::size_t SemMax = 30; struct GuestTimespec { std::int64_t seconds; @@ -101,21 +101,33 @@ int Wait(std::intptr_t id, bool once, const GuestTimespec* abstime) { } +bool IsKsemDescriptor(int descriptor) { + std::lock_guard lock(g_ksemLock); + return g_ksems.count(descriptor) != 0; +} + extern "C" { int APS5_VABI ksem_init_nid_postfix(std::intptr_t* idp, unsigned int value) { if (value > SemValueMax) return Fail(GuestEinval); - if (idp == nullptr) return Fail(GuestEfault); const int descriptor = ReserveDescriptor(); - if (descriptor < 0) return Fail(errno == ENFILE ? GuestEnfile : GuestEmfile); + if (descriptor < 0) return Fail(GuestEnospc); + if (idp == nullptr) { + close_nid_postfix(descriptor); + return Fail(GuestEfault); + } + *idp = descriptor; auto ksem = std::make_shared(); ksem->count = value; { std::lock_guard lock(g_ksemLock); - g_ksems[descriptor] = std::move(ksem); + if (g_ksems.size() < SemMax) { + g_ksems[descriptor] = std::move(ksem); + return 0; + } } - *idp = descriptor; - return 0; + close_nid_postfix(descriptor); + return Fail(GuestEnospc); } int APS5_VABI ksem_post_nid_postfix(std::intptr_t id) { diff --git a/core/libs/prx/libkernel_sys/CMakeLists.txt b/core/libs/prx/libkernel_sys/CMakeLists.txt index c35b590dae..70a25df4cd 100644 --- a/core/libs/prx/libkernel_sys/CMakeLists.txt +++ b/core/libs/prx/libkernel_sys/CMakeLists.txt @@ -1,6 +1,5 @@ add_library(${LIB_NAME} SHARED EXCLUDE_FROM_ALL Export.cpp Export.Discovery.cpp Aio.cpp Cpuset.cpp ThreadQueries.cpp) target_include_directories(${LIB_NAME} PRIVATE ${LIBS_INCLUDE_DIR}) -target_sources(${LIB_NAME} PRIVATE Ksem.cpp) configure_windows_unwind(${LIB_NAME}) target_sources(${LIB_NAME} PRIVATE Uuid.cpp) diff --git a/core/libs/tests/GuestKernelSysKsem.cpp b/core/libs/tests/GuestKernelSysKsem.cpp index 7a544d00c6..abb42ae461 100644 --- a/core/libs/tests/GuestKernelSysKsem.cpp +++ b/core/libs/tests/GuestKernelSysKsem.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -15,6 +16,7 @@ extern "C" { int* APS5_VABI __error_nid_postfix(); int APS5_VABI open_nid_postfix(const char* path, int flags, int mode); int APS5_VABI close_nid_postfix(int descriptor); +int APS5_VABI dup_nid_postfix(int descriptor); int APS5_VABI dup2_nid_postfix(int from, int to); int APS5_VABI ksem_init_nid_postfix(std::intptr_t* idp, unsigned int value); int APS5_VABI ksem_post_nid_postfix(std::intptr_t id); @@ -30,6 +32,7 @@ namespace { constexpr int Efault = 14; constexpr int Ebusy = 16; constexpr int Einval = 22; +constexpr int Enospc = 28; constexpr int Eagain = 35; constexpr int Etimedout = 60; constexpr int Eoverflow = 84; @@ -134,6 +137,39 @@ void Identifiers() { std::filesystem::remove(path); } +bool Throws(void (*call)(int), int argument) { + try { + call(argument); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +void Duplicates() { + std::intptr_t id = -1; + std::intptr_t target = -1; + Require(ksem_init_nid_postfix(&id, 1) == 0 && ksem_init_nid_postfix(&target, 1) == 0); + Require(Throws([](int descriptor) { dup_nid_postfix(descriptor); }, static_cast(id))); + static int destination = -1; + destination = static_cast(target); + Require(Throws([](int descriptor) { dup2_nid_postfix(descriptor, destination); }, static_cast(id))); + Require(dup2_nid_postfix(static_cast(id), static_cast(id)) == static_cast(id)); + Require(Value(id) == 1 && Value(target) == 1); + Require(ksem_destroy_nid_postfix(id) == 0 && ksem_destroy_nid_postfix(target) == 0); +} + +void Limit() { + std::intptr_t ids[30] = {}; + for (auto& id : ids) Require(ksem_init_nid_postfix(&id, 0) == 0); + std::intptr_t extra = -1; + Require(ksem_init_nid_postfix(&extra, 0) == -1 && Error() == Enospc && extra >= 0); + Require(ksem_post_nid_postfix(extra) == -1 && Error() == Einval); + Require(ksem_destroy_nid_postfix(ids[0]) == 0); + Require(ksem_init_nid_postfix(&ids[0], 0) == 0); + for (const auto id : ids) Require(ksem_destroy_nid_postfix(id) == 0); +} + } int main() { @@ -141,6 +177,8 @@ int main() { Timed(); Blocking(); Identifiers(); + Duplicates(); + Limit(); std::puts("ksem unnamed semaphores passed"); return 0; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 4968f46ca4..c27d693d79 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -21,6 +21,7 @@ - [sceKernelError](../../../core/libs/prx/libkernel/System/src/Error.cpp) follows [shadPS4's kernel implementation](https://github.com/shadps4-emu/shadPS4/blob/23c41333822fe8eca4c609b43119180af63ae4ac/src/core/libraries/kernel/kernel.cpp): zero returns zero; other guest POSIX error values receive the `0x80020000` bias without modifying errno. Arithmetic is defined modulo 32 bits to avoid signed overflow for inputs outside the errno domain. Console behavior for those inputs and PS5 equivalence have not been measured. - [physhm_open, physhm_unlink](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel, NIDs `n371J5cP+uo` and `AUqJNkobQ1c`) - declared as throwing stubs: the PS5 physical shared memory API is not documented, and [shadPS4's module generator](https://github.com/shadps4-emu/moduleGenerator/blob/main/libraries/kernel.cpp) also declares both with no arguments and returns `ORBIS_OK`, so the signature, the handle a success would return and the console's behaviour are unmeasured. The no-argument `int` signature follows shadPS4. Forever Launcher (PPSA99030) imports both and stops at load on `physhm_open`, and the relinker resolves no call site for either in that title, so no handle is fabricated and both throw +- [ksem_init, ksem_post, ksem_wait, ksem_trywait, ksem_timedwait, ksem_getvalue and ksem_destroy](../../../core/libs/prx/libkernel/Semaphore/src/Ksem.cpp) (libkernel) - `dup` of a ksem id, or `dup2` from one, throws, where FreeBSD shares the semaphore, and a guest signal does not interrupt a wait with `EINTR`. Otherwise they follow FreeBSD 11 `uipc_sem.c`: each semaphore is kept in a table keyed by a descriptor reserved on the host null device, as a ksem id is a descriptor there; a guest `close`, or `dup2` over the id, drops it through libkernel's close notification; `ksem_init` fails with `ENOSPC` when no descriptor is left or 30 semaphores (`SEM_MAX`) exist - [getlogin_r](../../../core/libs/prx/libkernel/System/src/Login.cpp) (libkernel) returns the host effective account name: `GetUserNameW` as UTF-8 on Windows and `getpwuid_r(geteuid())` on POSIX. A valid account returns a name even without a login session; host `getlogin_r`, `USER` and `LOGNAME` are not consulted. The console's login name and behaviour without a session are unknown. Account lookup or encoding failures, a missing or empty account name, and POSIX account records exceeding the 16 KiB lookup buffer throw; there is no replacement name or retry. A short guest buffer returns `ERANGE` without modifying it, and a null output returns `EFAULT`. - [getlogin](../../../core/libs/prx/libkernel/System/src/Getlogin.cpp) (libkernel) follows FreeBSD 11 `lib/libc/gen/getlogin.c` and returns a pointer to one static 33-byte buffer filled by `getlogin_r`; on an error it sets `errno` to `getlogin_r`'s code and returns NULL. These semantics are not checked against a PS5. It exports the test seam `LoginNameForTest_nid_no_patch`, which replaces the host account name for tests. - [setlogin](../../../core/libs/prx/libkernel/System/src/Login.cpp) (libkernel_sys) keeps one name for the process, where FreeBSD 11.4's [`sys_setlogin`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_prot.c#L2138-L2161) sets it for the session, and does not check `PRIV_PROC_SETLOGIN`, which a jailed process fails with `EPERM`; the console's answer is not known. As in FreeBSD, a null name fails with `EFAULT` and a name of 33 bytes or more (`MAXLOGNAME` with the NUL) with `EINVAL`, and nothing is stored. Once a name is stored, `getlogin_r` returns it instead of the host account name described for `getlogin_r`, and ahead of the `LoginNameForTest_nid_no_patch` seam. The name is copied from the guest with the same 33-byte bound as `copyinstr`, so a name with no NUL in its first 33 bytes fails with `EINVAL` without reading past them. From 0e693817d8056c581fbbe228b97d0cbfd633b58a Mon Sep 17 00:00:00 2001 From: David Date: Wed, 21 Oct 2026 22:28:22 +0100 Subject: [PATCH 075/160] docs(debt): list the unnamed ksem calls under Functional --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index c27d693d79..0370ff465a 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -21,7 +21,6 @@ - [sceKernelError](../../../core/libs/prx/libkernel/System/src/Error.cpp) follows [shadPS4's kernel implementation](https://github.com/shadps4-emu/shadPS4/blob/23c41333822fe8eca4c609b43119180af63ae4ac/src/core/libraries/kernel/kernel.cpp): zero returns zero; other guest POSIX error values receive the `0x80020000` bias without modifying errno. Arithmetic is defined modulo 32 bits to avoid signed overflow for inputs outside the errno domain. Console behavior for those inputs and PS5 equivalence have not been measured. - [physhm_open, physhm_unlink](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel, NIDs `n371J5cP+uo` and `AUqJNkobQ1c`) - declared as throwing stubs: the PS5 physical shared memory API is not documented, and [shadPS4's module generator](https://github.com/shadps4-emu/moduleGenerator/blob/main/libraries/kernel.cpp) also declares both with no arguments and returns `ORBIS_OK`, so the signature, the handle a success would return and the console's behaviour are unmeasured. The no-argument `int` signature follows shadPS4. Forever Launcher (PPSA99030) imports both and stops at load on `physhm_open`, and the relinker resolves no call site for either in that title, so no handle is fabricated and both throw -- [ksem_init, ksem_post, ksem_wait, ksem_trywait, ksem_timedwait, ksem_getvalue and ksem_destroy](../../../core/libs/prx/libkernel/Semaphore/src/Ksem.cpp) (libkernel) - `dup` of a ksem id, or `dup2` from one, throws, where FreeBSD shares the semaphore, and a guest signal does not interrupt a wait with `EINTR`. Otherwise they follow FreeBSD 11 `uipc_sem.c`: each semaphore is kept in a table keyed by a descriptor reserved on the host null device, as a ksem id is a descriptor there; a guest `close`, or `dup2` over the id, drops it through libkernel's close notification; `ksem_init` fails with `ENOSPC` when no descriptor is left or 30 semaphores (`SEM_MAX`) exist - [getlogin_r](../../../core/libs/prx/libkernel/System/src/Login.cpp) (libkernel) returns the host effective account name: `GetUserNameW` as UTF-8 on Windows and `getpwuid_r(geteuid())` on POSIX. A valid account returns a name even without a login session; host `getlogin_r`, `USER` and `LOGNAME` are not consulted. The console's login name and behaviour without a session are unknown. Account lookup or encoding failures, a missing or empty account name, and POSIX account records exceeding the 16 KiB lookup buffer throw; there is no replacement name or retry. A short guest buffer returns `ERANGE` without modifying it, and a null output returns `EFAULT`. - [getlogin](../../../core/libs/prx/libkernel/System/src/Getlogin.cpp) (libkernel) follows FreeBSD 11 `lib/libc/gen/getlogin.c` and returns a pointer to one static 33-byte buffer filled by `getlogin_r`; on an error it sets `errno` to `getlogin_r`'s code and returns NULL. These semantics are not checked against a PS5. It exports the test seam `LoginNameForTest_nid_no_patch`, which replaces the host account name for tests. - [setlogin](../../../core/libs/prx/libkernel/System/src/Login.cpp) (libkernel_sys) keeps one name for the process, where FreeBSD 11.4's [`sys_setlogin`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_prot.c#L2138-L2161) sets it for the session, and does not check `PRIV_PROC_SETLOGIN`, which a jailed process fails with `EPERM`; the console's answer is not known. As in FreeBSD, a null name fails with `EFAULT` and a name of 33 bytes or more (`MAXLOGNAME` with the NUL) with `EINVAL`, and nothing is stored. Once a name is stored, `getlogin_r` returns it instead of the host account name described for `getlogin_r`, and ahead of the `LoginNameForTest_nid_no_patch` seam. The name is copied from the guest with the same 33-byte bound as `copyinstr`, so a name with no NUL in its first 33 bytes fails with `EINVAL` without reading past them. @@ -188,6 +187,7 @@ - [The sandbox CA list](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) (libkernel), `//common/cert/CA_LIST.cer`, is a PEM bundle of the host's trust anchors, written once per process to `cert_cache/CA_LIST.cer` in the run directory with the owner write bit cleared: on Windows the current user's `ROOT` store, filtered through Disallowed and the combined EKU extension/property for TLS server authentication (Windows fetches missing roots on demand, so a root that was never fetched is absent), elsewhere `SSL_CERT_FILE` or the first of `/etc/ssl/certs/ca-certificates.crt`, `/etc/pki/tls/certs/ca-bundle.crt`, `/etc/ssl/ca-bundle.pem` and `/etc/ssl/cert.pem` that exists. On macOS, `/etc/ssl/cert.pem` is a static snapshot and does not reflect roots added to or distrusted in the Keychain. Existing guest files, explicit aliases and blocked mounts take precedence over the provider. The PEM snapshot does not carry Windows CTL restrictions beyond EKU, native revocation policy or later trust-store changes. Without any certificate the path does not exist. The console file's format is unverified: only that bundled OpenSSL accepts a PEM bundle there is shown (ProsperoRadio PPSA99001 and SwanStation PPSA98510, #5001). The other entries of the console's `common/cert` directory, the directory itself (it does not exist here) and the error a write open gets (`EACCES` from the host here) are unverified - [pselect](../../../core/libs/prx/libkernel/Socket/src/Pselect.cpp) (libkernel) sets the signal mask, calls `select` and restores the mask, as FreeBSD 11 `kern_pselect` does. A pending signal that the new mask unblocks has its handler run during the swap, and `pselect` then returns `EINTR` unless a descriptor is already ready, the timeout is zero or an argument is invalid, as FreeBSD's `kern_select` decides before it sleeps. A signal raised during the wait, including one sent by another thread, does not interrupt it, because `select` has no `EINTR` path; and the mask is the process-wide one, so it applies to every thread for the duration of the call - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), the layout homebrew built against FreeBSD's headers passes: PPSA99039's OpenSSL `ASYNC` (`crypto/async/arch/async_posix.c`) captures into a 0x370-byte stack buffer. `getcontext` writes the 0x330 bytes before `uc_link`, as FreeBSD's `UC_COPY_SIZE`. The console's own layout for these functions is not verified; the exception handler's context is a different one (see `sceKernelRaiseException`) +- [ksem_init, ksem_post, ksem_wait, ksem_trywait, ksem_timedwait, ksem_getvalue and ksem_destroy](../../../core/libs/prx/libkernel/Semaphore/src/Ksem.cpp) (libkernel) - `dup` of a ksem id, or `dup2` from one, throws, where FreeBSD shares the semaphore, and a guest signal does not interrupt a wait with `EINTR`. Otherwise they follow FreeBSD 11 `uipc_sem.c`: each semaphore is kept in a table keyed by a descriptor reserved on the host null device, as a ksem id is a descriptor there; a guest `close`, or `dup2` over the id, drops it through libkernel's close notification; `ksem_init` fails with `ENOSPC` when no descriptor is left or 30 semaphores (`SEM_MAX`) exist - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) keep the 512-byte FXSAVE image in `mc_fpstate` (x87, MXCSR, XMM0-15), so the upper halves of YMM0-15 are neither saved nor restored, and `getcontext` leaves `mc_xfpustate` and `mc_xfpustate_len` 0. `getcontext` sets `mc_fpformat` and `mc_ownedfp` to FreeBSD's `_MC_FPFMT_XMM` and `_MC_FPOWNED_FPU`, `mc_cs` and `mc_ss` to the host selectors, and `mc_flags`, `mc_fsbase` and `mc_gsbase` to 0. `setcontext` loads `mc_fpstate` whatever `mc_fpformat` and `mc_ownedfp` say, and restores neither the segment selectors, `mc_fsbase` and `mc_gsbase` nor the extended state, which FreeBSD restores when `mc_flags` asks for them - [setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) on Windows set the thread's stack bounds in the TEB to the stack they switch to: a `makecontext` stack, or else the committed region around the target stack pointer. A switch to any other guest stack, such as a context built by hand, gets the bounds of that region only - [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) and [pthread_attr_get_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - the answer for a thread that has ended is not measured on the console; the implementation follows FreeBSD 11.4's [`_pthread_attr_get_np`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L139-L172) through `_thr_find_thread`. On Windows a host thread adopted by `scePthreadSelf` that ends without `scePthreadExit` keeps answering 0. Monster Hunter Wilds (PPSA24191) reuses a slot of its thread table only when `scePthreadAttrGet` returns `0x80020003` From 735dd0bdd7164307f16e873a6a783ee8c6c9dfe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= Date: Wed, 28 Oct 2026 12:58:01 +0100 Subject: [PATCH 076/160] fix(libkernel): declare the unnamed ksem exports once in the public header Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/Equeue/Equeue.hpp | 1 + .../libs/prx/libkernel/Semaphore/src/Ksem.cpp | 20 +++++--------- core/libs/prx/libkernel/include/Api.hpp | 7 +++++ core/libs/prx/libkernel_sys/include/Api.hpp | 7 ----- core/libs/tests/GuestKernelSysKsem.cpp | 27 +++---------------- 5 files changed, 19 insertions(+), 43 deletions(-) diff --git a/core/libs/prx/libkernel/Equeue/Equeue.hpp b/core/libs/prx/libkernel/Equeue/Equeue.hpp index 4cace734bd..52adc3693f 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.hpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.hpp @@ -132,6 +132,7 @@ void KqueueDescriptorClosed(int descriptor); void EqueueDescriptorReadShut(int descriptor); void KqueueDescriptorReadShut(int descriptor); extern "C" bool IsKqueueDescriptor_nid_no_patch(int descriptor); +extern "C" void RegisterDescriptorCloseObserver_nid_no_patch(void (*observer)(int descriptor)); std::optional KqueueReadable(int descriptor); KernelEqueueEvent EqueueDescriptorEvent(int descriptor, int16_t filter, uint16_t flags, void* udata); void RequireVnodeNotes(uint32_t fflags); diff --git a/core/libs/prx/libkernel/Semaphore/src/Ksem.cpp b/core/libs/prx/libkernel/Semaphore/src/Ksem.cpp index 743b919295..6ef23ccd2c 100644 --- a/core/libs/prx/libkernel/Semaphore/src/Ksem.cpp +++ b/core/libs/prx/libkernel/Semaphore/src/Ksem.cpp @@ -1,5 +1,8 @@ #include "SceTypes.hpp" +#include "prx/libc/include/Api.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/Equeue/Equeue.hpp" +#include "prx/libkernel/include/Api.hpp" #include "prx/libkernel/Semaphore/include/Ksem.hpp" #include #include @@ -16,10 +19,6 @@ #include #endif -extern "C" int* APS5_VABI __error_nid_postfix(); -extern "C" int APS5_VABI close_nid_postfix(int descriptor); -extern "C" void RegisterDescriptorCloseObserver_nid_no_patch(void (*observer)(int descriptor)); - namespace { constexpr int GuestEfault = 14; @@ -32,11 +31,6 @@ constexpr int GuestEoverflow = 84; constexpr unsigned SemValueMax = 0x7fffffffu; constexpr std::size_t SemMax = 30; -struct GuestTimespec { - std::int64_t seconds; - std::int64_t nanoseconds; -}; - struct Ksem { std::mutex lock; std::condition_variable available; @@ -77,7 +71,7 @@ const bool g_observerRegistered = [] { return true; }(); -int Wait(std::intptr_t id, bool once, const GuestTimespec* abstime) { +int Wait(std::intptr_t id, bool once, const KernelTimespec* abstime) { const auto ksem = Find(id); if (!ksem) return Fail(GuestEinval); std::unique_lock lock(ksem->lock); @@ -89,7 +83,7 @@ int Wait(std::intptr_t id, bool once, const GuestTimespec* abstime) { ksem->available.wait(lock, [&] { return ksem->count > 0; }); } else { const auto deadline = std::chrono::system_clock::time_point(std::chrono::duration_cast( - std::chrono::seconds(abstime->seconds) + std::chrono::nanoseconds(abstime->nanoseconds))); + std::chrono::seconds(abstime->tv_sec) + std::chrono::nanoseconds(abstime->tv_nsec))); ready = ksem->available.wait_until(lock, deadline, [&] { return ksem->count > 0; }); } --ksem->waiters; @@ -148,8 +142,8 @@ int APS5_VABI ksem_trywait_nid_postfix(std::intptr_t id) { return Wait(id, true, nullptr); } -int APS5_VABI ksem_timedwait_nid_postfix(std::intptr_t id, const GuestTimespec* abstime) { - if (abstime != nullptr && (abstime->nanoseconds < 0 || abstime->nanoseconds >= 1000000000)) return Fail(GuestEinval); +int APS5_VABI ksem_timedwait_nid_postfix(std::intptr_t id, const KernelTimespec* abstime) { + if (abstime != nullptr && (abstime->tv_nsec < 0 || abstime->tv_nsec >= 1000000000)) return Fail(GuestEinval); return Wait(id, false, abstime); } diff --git a/core/libs/prx/libkernel/include/Api.hpp b/core/libs/prx/libkernel/include/Api.hpp index 5451da0393..7b7d27398e 100644 --- a/core/libs/prx/libkernel/include/Api.hpp +++ b/core/libs/prx/libkernel/include/Api.hpp @@ -473,6 +473,13 @@ int APS5_VABI pthread_rwlock_trywrlock_nid_postfix(PthreadRwlock* rwlock); int APS5_VABI pthread_rwlock_unlock_nid_postfix(PthreadRwlock* rwlock); int APS5_VABI pthread_rwlock_wrlock_nid_postfix(PthreadRwlock* rwlock); int APS5_VABI pthread_rwlock_setname_np_nid_postfix(PthreadRwlock* rwlock, const char* name); +int APS5_VABI ksem_init_nid_postfix(std::intptr_t* idp, unsigned int value); +int APS5_VABI ksem_post_nid_postfix(std::intptr_t id); +int APS5_VABI ksem_wait_nid_postfix(std::intptr_t id); +int APS5_VABI ksem_trywait_nid_postfix(std::intptr_t id); +int APS5_VABI ksem_timedwait_nid_postfix(std::intptr_t id, const KernelTimespec* abstime); +int APS5_VABI ksem_getvalue_nid_postfix(std::intptr_t id, int* value); +int APS5_VABI ksem_destroy_nid_postfix(std::intptr_t id); int APS5_VABI sem_init_nid_postfix(void* sem, int pshared, unsigned int value); int APS5_VABI sem_destroy_nid_postfix(void* sem); int APS5_VABI sem_wait_nid_postfix(void* sem); diff --git a/core/libs/prx/libkernel_sys/include/Api.hpp b/core/libs/prx/libkernel_sys/include/Api.hpp index fe88b9f12f..072b161261 100644 --- a/core/libs/prx/libkernel_sys/include/Api.hpp +++ b/core/libs/prx/libkernel_sys/include/Api.hpp @@ -124,15 +124,8 @@ int APS5_VABI kmq_timedreceive_nid_postfix(void); int APS5_VABI kmq_timedsend_nid_postfix(void); int APS5_VABI kmq_unlink_nid_postfix(void); int APS5_VABI ksem_close_nid_postfix(void); -int APS5_VABI ksem_destroy_nid_postfix(void); -int APS5_VABI ksem_getvalue_nid_postfix(void); -int APS5_VABI ksem_init_nid_postfix(void); int APS5_VABI ksem_open_nid_postfix(void); -int APS5_VABI ksem_post_nid_postfix(void); -int APS5_VABI ksem_timedwait_nid_postfix(void); -int APS5_VABI ksem_trywait_nid_postfix(void); int APS5_VABI ksem_unlink_nid_postfix(void); -int APS5_VABI ksem_wait_nid_postfix(void); int APS5_VABI ktrace_nid_postfix(void); int APS5_VABI mdbg_call_nid_postfix(void); int APS5_VABI mdbg_service_nid_postfix(void); diff --git a/core/libs/tests/GuestKernelSysKsem.cpp b/core/libs/tests/GuestKernelSysKsem.cpp index abb42ae461..2fe006953d 100644 --- a/core/libs/tests/GuestKernelSysKsem.cpp +++ b/core/libs/tests/GuestKernelSysKsem.cpp @@ -1,5 +1,6 @@ #include "SceTypes.hpp" -#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libkernel/include/Api.hpp" #include #include #include @@ -12,21 +13,6 @@ #include #include -extern "C" { -int* APS5_VABI __error_nid_postfix(); -int APS5_VABI open_nid_postfix(const char* path, int flags, int mode); -int APS5_VABI close_nid_postfix(int descriptor); -int APS5_VABI dup_nid_postfix(int descriptor); -int APS5_VABI dup2_nid_postfix(int from, int to); -int APS5_VABI ksem_init_nid_postfix(std::intptr_t* idp, unsigned int value); -int APS5_VABI ksem_post_nid_postfix(std::intptr_t id); -int APS5_VABI ksem_wait_nid_postfix(std::intptr_t id); -int APS5_VABI ksem_trywait_nid_postfix(std::intptr_t id); -int APS5_VABI ksem_timedwait_nid_postfix(std::intptr_t id, const void* abstime); -int APS5_VABI ksem_getvalue_nid_postfix(std::intptr_t id, int* value); -int APS5_VABI ksem_destroy_nid_postfix(std::intptr_t id); -} - namespace { constexpr int Efault = 14; @@ -37,11 +23,6 @@ constexpr int Eagain = 35; constexpr int Etimedout = 60; constexpr int Eoverflow = 84; -struct Timespec { - std::int64_t seconds; - std::int64_t nanoseconds; -}; - void Require(bool condition, std::source_location location = std::source_location::current()) { if (condition) return; std::fprintf(stderr, "check failed at line %u\n", static_cast(location.line())); @@ -52,7 +33,7 @@ int Error() { return *__error_nid_postfix(); } -Timespec FromNow(std::chrono::milliseconds delay) { +KernelTimespec FromNow(std::chrono::milliseconds delay) { const auto when = std::chrono::system_clock::now() + delay; const auto nanoseconds = std::chrono::duration_cast(when.time_since_epoch()).count(); return {nanoseconds / 1000000000, nanoseconds % 1000000000}; @@ -84,7 +65,7 @@ void Counting() { void Timed() { std::intptr_t id = -1; Require(ksem_init_nid_postfix(&id, 0) == 0); - const Timespec invalid{0, 1000000000}; + const KernelTimespec invalid{0, 1000000000}; Require(ksem_timedwait_nid_postfix(id, &invalid) == -1 && Error() == Einval); const auto deadline = FromNow(std::chrono::milliseconds(150)); const auto start = std::chrono::steady_clock::now(); From 91d8280a290f94786a1c356220431b112a4fb6bd Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 28 Oct 2026 12:58:51 +0100 Subject: [PATCH 077/160] test(libkernel): do not lower RLIMIT_DATA under AddressSanitizer in guest_getrlimit LoweredHostLimitIsReported sets RLIMIT_DATA to 1 TiB. An AddressSanitizer build already has about 14 TiB of shadow mapped, so the next mapping the sanitizer makes fails with ENOMEM and the test aborts. Skip that block when ANYPS5_ENABLE_ASAN is set and say so on stderr, as GuestAllocatorFailures does; every other check of the test still runs. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestGetrlimit.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/libs/tests/GuestGetrlimit.cpp b/core/libs/tests/GuestGetrlimit.cpp index 9f2a39f589..b58463498e 100644 --- a/core/libs/tests/GuestGetrlimit.cpp +++ b/core/libs/tests/GuestGetrlimit.cpp @@ -3,6 +3,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include #include @@ -149,6 +150,10 @@ static void RequireHostLimit(int resource, int hostResource) { } static void LoweredHostLimitIsReported() { +#ifdef ANYPS5_ENABLE_ASAN + std::fputs("skipped: AddressSanitizer keeps about 14 TiB of shadow mapped, so lowering RLIMIT_DATA to 1 TiB makes its next mapping fail\n", stderr); + return; +#else rlimit host{}; Require(getrlimit(RLIMIT_DATA, &host) == 0); const rlim_t lowered = rlim_t{1} << 40; @@ -156,6 +161,7 @@ static void LoweredHostLimitIsReported() { host.rlim_cur = lowered; Require(setrlimit(RLIMIT_DATA, &host) == 0); Require(GuestLimit(GUEST_RLIMIT_DATA).rlim_cur == static_cast(lowered)); +#endif } #endif From 0bec3390210837904ec755faa10bf903223d86e3 Mon Sep 17 00:00:00 2001 From: Get-fix <100056769+Get-fix@users.noreply.github.com> Date: Sat, 17 Oct 2026 05:04:19 +0300 Subject: [PATCH 078/160] feat(libSceAgcDriver): draw PS4 (GCN) command buffers PS4 color and depth targets, PS4 graphics EOP events, and the PS4 tile modes (1D/2D thin and display, depth with tile split, PRT, linear general) on the AGC executor, with a GPU detiler for every mode. Textures sampled over the padded corner of a render target are GPU block copies, and the PS4 SDK pattern-fill compute kernel runs as a GPU fill when it repeats one dword, so render targets and HTILE are cleared without a CPU drain. Two-dword patterns keep running the kernel: as fills, Limbo's clears of its 64-bit target made its gameplay advance about half as far per frame. Tests cover the tiling equations against addrlib formulas, the GPU detiler for every mode and element size, and the fill ranges. --- .../prx/libSceAgcDriver/Eq/include/Event.hpp | 2 + .../libs/prx/libSceAgcDriver/Eq/src/Event.cpp | 87 +++++--- .../Execution/include/DisplayBuffer.hpp | 2 + .../Execution/include/Driver.hpp | 2 + .../include/Driver/Memory/BufferFill.hpp | 24 +++ .../Execution/src/DisplayBuffer.cpp | 23 +- .../Execution/src/Driver/Draw/DrawDecode.cpp | 4 +- .../src/Driver/Draw/DrawRegisterKey.cpp | 11 +- .../Execution/src/Driver/EntryPoints.cpp | 10 + .../src/Driver/Memory/BufferFill.cpp | 6 +- .../src/Driver/Shaders/ShaderRegistry.cpp | 3 +- .../Execution/src/VulkanDevice.cpp | 4 +- .../Graphics/include/ColorTargetLayout.hpp | 20 +- .../Graphics/include/GuestTextureResource.hpp | 44 ++++ .../Graphics/include/State.hpp | 7 +- .../Graphics/include/Texture.hpp | 1 + .../Graphics/include/TextureTiling.hpp | 17 ++ .../Graphics/shaders/TextureDetile.comp | 9 + .../Graphics/src/ColorTargetLayout.cpp | 20 ++ .../Graphics/src/DepthSurface.cpp | 3 + .../Graphics/src/GuestTextureResource.cpp | 44 +++- .../libSceAgcDriver/Graphics/src/State.cpp | 75 ++++++- .../libSceAgcDriver/Graphics/src/Texture.cpp | 18 +- .../Graphics/src/TextureDetiler.cpp | 23 +- .../Graphics/src/TextureTiling.cpp | 204 +++++++++++++++++- .../prx/libSceAgcDriver/tests/RegisterKey.cpp | 10 +- .../libSceAgcDriver/tests/TextureDetiler.cpp | 1 + .../libSceAgcDriver/tests/TextureTiling.cpp | 135 ++++++++++++ .../tests/execution/GcnDetile.cpp | 91 ++++++++ .../tests/execution/PatternBufferFill.cpp | 24 +++ .../libSceAgcDriver/tools/ShaderReplay.cpp | 4 + docs/dev/debt/libSceAgcDriver.md | 4 + 32 files changed, 852 insertions(+), 80 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/GcnDetile.cpp diff --git a/core/libs/prx/libSceAgcDriver/Eq/include/Event.hpp b/core/libs/prx/libSceAgcDriver/Eq/include/Event.hpp index 2b7bcbe4b0..3f25b907df 100644 --- a/core/libs/prx/libSceAgcDriver/Eq/include/Event.hpp +++ b/core/libs/prx/libSceAgcDriver/Eq/include/Event.hpp @@ -8,5 +8,7 @@ bool AgcDriverDeliverEopInterrupt(std::uint32_t queue); extern "C" int APS5_VABI sceAgcDriverAddEqEvent(KernelEqueue eq, int id, void* udata); extern "C" int APS5_VABI sceAgcDriverDeleteEqEvent(KernelEqueue eq, int id); +extern "C" int AgcDriverAddQueueEqEvent_nid_postfix(KernelEqueue eq, std::uint32_t queue, std::uintptr_t ident, void* udata); +extern "C" int AgcDriverDeleteQueueEqEvent_nid_postfix(KernelEqueue eq, std::uintptr_t ident); #endif diff --git a/core/libs/prx/libSceAgcDriver/Eq/src/Event.cpp b/core/libs/prx/libSceAgcDriver/Eq/src/Event.cpp index d7f5463eae..035952a617 100644 --- a/core/libs/prx/libSceAgcDriver/Eq/src/Event.cpp +++ b/core/libs/prx/libSceAgcDriver/Eq/src/Event.cpp @@ -18,38 +18,19 @@ constexpr int16_t EvfiltGraphicsCore = -14; struct Registration { KernelEqueue eq; - int id; + std::uint32_t queue; + std::uintptr_t ident; }; AgcDriver::Mutex g_mutex; std::vector g_registrations; -} - -bool AgcDriverDeliverEopInterrupt(std::uint32_t queue) { - std::vector registrations; - { - std::lock_guard lock(g_mutex); - registrations = g_registrations; - } - bool delivered = false; - for (const auto& registration : registrations) { - if (registration.id == static_cast(queue)) { - EqueueTriggerEvent_nid_postfix(registration.eq, static_cast(registration.id), EvfiltGraphicsCore, nullptr); - delivered = true; - } - } - return delivered; -} - -extern "C" { - -int APS5_VABI sceAgcDriverAddEqEvent(KernelEqueue eq, int id, void* udata) { +int AddEvent(KernelEqueue eq, std::uint32_t queue, std::uintptr_t ident, void* udata, const char* caller) { if (eq == 0 || !EqueuePin_nid_postfix(eq)) { - throw std::runtime_error(std::string(__func__) + ": invalid event queue"); + throw std::runtime_error(std::string(caller) + ": invalid event queue"); } KernelEqueueEvent event{}; - event.event.ident = static_cast(id); + event.event.ident = ident; event.event.filter = EvfiltGraphicsCore; event.event.flags = EV_ADD | EV_CLEAR; event.event.udata = udata; @@ -63,24 +44,60 @@ int APS5_VABI sceAgcDriverAddEqEvent(KernelEqueue eq, int id, void* udata) { }; const int result = EqueueAddEvent_nid_postfix(eq, event); if (result != EQUEUE_OK) { - throw std::runtime_error(std::string(__func__) + ": event queue rejected the event"); + throw std::runtime_error(std::string(caller) + ": event queue rejected the event"); } std::lock_guard lock(g_mutex); - if (std::none_of(g_registrations.begin(), g_registrations.end(), [&](const Registration& r) { return r.eq == eq && r.id == id; })) { - g_registrations.push_back({eq, id}); + if (std::none_of(g_registrations.begin(), g_registrations.end(), [&](const Registration& r) { return r.eq == eq && r.ident == ident; })) { + g_registrations.push_back({eq, queue, ident}); } return 0; } +int DeleteEvent(KernelEqueue eq, std::uintptr_t ident, const char* caller) { + { + std::lock_guard lock(g_mutex); + std::erase_if(g_registrations, [&](const Registration& r) { return r.eq == eq && r.ident == ident; }); + } + if (EqueueDeleteEvent_nid_postfix(eq, ident, EvfiltGraphicsCore) != EQUEUE_OK) { + throw std::runtime_error(std::string(caller) + ": event is not registered"); + } + return 0; +} + +} + +bool AgcDriverDeliverEopInterrupt(std::uint32_t queue) { + std::vector registrations; + { + std::lock_guard lock(g_mutex); + registrations = g_registrations; + } + bool delivered = false; + for (const auto& registration : registrations) { + if (registration.queue == queue) { + EqueueTriggerEvent_nid_postfix(registration.eq, registration.ident, EvfiltGraphicsCore, nullptr); + delivered = true; + } + } + return delivered; +} + +extern "C" { + +int APS5_VABI sceAgcDriverAddEqEvent(KernelEqueue eq, int id, void* udata) { + return AddEvent(eq, static_cast(id), static_cast(id), udata, __func__); +} + int APS5_VABI sceAgcDriverDeleteEqEvent(KernelEqueue eq, int id) { - { - std::lock_guard lock(g_mutex); - std::erase_if(g_registrations, [&](const Registration& r) { return r.eq == eq && r.id == id; }); - } - if (EqueueDeleteEvent_nid_postfix(eq, static_cast(id), EvfiltGraphicsCore) != EQUEUE_OK) { - throw std::runtime_error(std::string(__func__) + ": event is not registered"); - } - return 0; + return DeleteEvent(eq, static_cast(id), __func__); } } + +extern "C" int AgcDriverAddQueueEqEvent_nid_postfix(KernelEqueue eq, std::uint32_t queue, std::uintptr_t ident, void* udata) { + return AddEvent(eq, queue, ident, udata, __func__); +} + +extern "C" int AgcDriverDeleteQueueEqEvent_nid_postfix(KernelEqueue eq, std::uintptr_t ident) { + return DeleteEvent(eq, ident, __func__); +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp index 685976d255..96aa902041 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp @@ -25,6 +25,8 @@ struct DisplayBuffer { std::uint32_t alignment = 65536; }; +inline constexpr std::uint32_t DisplayTilingGcn = 2; + std::size_t DisplayBufferSize(const DisplayBuffer& buffer); std::size_t DisplayBufferOffset(const DisplayBuffer& buffer, std::uint32_t x, std::uint32_t y); std::vector DecodeDisplayBuffer(const DisplayBuffer& buffer, std::span source); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp index 03e68cd1b7..7955e5e5f1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp @@ -24,6 +24,8 @@ struct AwaitedWait { extern "C" void AgcDriverPrepareCommandWrite_nid_no_patch(std::uint64_t address, std::size_t bytes); extern "C" void AgcDriverWaitIdle_nid_postfix(); +extern "C" void AgcDriverSubmitGraphics_nid_postfix(std::uint32_t* commands, std::uint32_t dwords); +extern "C" void AgcDriverUseGcnShaders_nid_postfix(); extern "C" void AgcDriverRaiseWorkerThreadPriority_nid_postfix(const char* role); extern "C" void AgcDriverLockVulkanLoader_nid_postfix(); extern "C" void AgcDriverUnlockVulkanLoader_nid_postfix(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferFill.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferFill.hpp index 7b27661f98..89222cb247 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferFill.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferFill.hpp @@ -40,6 +40,30 @@ struct PatternFillRange { } }; +inline constexpr std::array GcnPatternFillKernel{ + 0xbeeb03ffu, 0x0000000bu, 0x8f008610u, 0x4a000000u, 0xc2400d00u, 0xbf8c007fu, + 0x7d880000u, 0xbe82246au, 0x36020001u, 0xbf880007u, 0xe0002000u, 0x80010101u, + 0x7d88000au, 0xbe80246au, 0xbf8c0f70u, 0xe0102000u, 0x80020100u, 0xbf810000u +}; + +inline bool MatchesGcnPatternFillKernel(std::span code, std::span userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) { + if (userData.size() != 16 || compute.numThreads != std::array{64, 1, 1} || !compute.groupIdEnable[0] || compute.threadIdComponentCount != 1) return false; + if (code.size() < GcnPatternFillKernel.size() || !std::equal(GcnPatternFillKernel.begin(), GcnPatternFillKernel.end(), code.begin())) return false; + for (const auto first : {4u, 8u}) { + if ((userData[first + 1] & 0xbfff0000u) != 0x00040000u || (userData[first + 3] & 0xc087f000u) != 0x00024000u) return false; + } + return (userData[7] & 7u) == 4u && userData[6] >= 1 && userData[14] >= 1; +} + +inline std::optional DecodeGcnPatternFillRange(std::span userData, std::span packet) { + if (userData.size() != 16 || packet.size() < 5 || packet[2] != 1 || packet[3] != 1) return std::nullopt; + const std::uint64_t invocations = (packet[4] & 0x20u) != 0 ? packet[1] : std::uint64_t{packet[1]} * 64u; + if (invocations > (std::uint64_t{1} << 32u)) return std::nullopt; + const auto base = [&](std::size_t word) { return userData[word] | (static_cast(userData[word + 1] & 0xfffu) << 32u); }; + if (base(4) % 4 != 0 || base(12) % 4 != 0) return std::nullopt; + return PatternFillRange{base(4), base(8), base(12), invocations, userData[10]}; +} + inline std::optional DecodePatternFillRange(std::span userData, std::span packet) { if (userData.size() != 12 || packet.size() < 5 || packet[2] != 1 || packet[3] != 1) return std::nullopt; const std::uint64_t invocations = (packet[4] & 0x20u) != 0 ? packet[1] : std::uint64_t{packet[1]} * 64u; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp index 6f7dd42ade..85128f0b68 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/HostHdr.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/HalfFloatScanout.hpp" #include @@ -12,6 +13,7 @@ #include #include #include +#include #include #include @@ -39,7 +41,19 @@ std::uint32_t tileOffset64(std::uint32_t x, std::uint32_t y) { return ((x << 3u) & 0x0008u) ^ ((x << 4u) & 0x0460u) ^ ((x << 5u) & 0x0300u) ^ ((x << 6u) & 0x0800u) ^ ((x << 9u) & 0x8000u) ^ ((x << 10u) & 0x2000u) ^ ((y << 4u) & 0x0010u) ^ ((y << 6u) & 0x0080u) ^ ((y << 5u) & 0x0f00u) ^ ((y << 10u) & 0x5000u); } +const Graphics::ColorTargetLayout& gcnLayout(const DisplayBuffer& buffer, std::uint32_t texelBytes) { + thread_local std::optional layout; + thread_local std::array key{}; + const std::array wanted{buffer.pitchInPixel, buffer.height, texelBytes}; + if (!layout || key != wanted) { + layout.emplace(buffer.pitchInPixel, buffer.height, Graphics::ColorTileMode::GcnDisplay2D, texelBytes); + key = wanted; + } + return *layout; +} + std::size_t texelOffset(const DisplayBuffer& buffer, std::uint32_t x, std::uint32_t y, std::uint32_t texelBytes) { + if (buffer.tilingMode == DisplayTilingGcn) return gcnLayout(buffer, texelBytes).Offset(x, y); if (buffer.tilingMode == 1) { const auto pitch = buffer.pitchInPixel == 0 ? buffer.width : buffer.pitchInPixel; return (static_cast(y) * pitch + x) * texelBytes; @@ -130,9 +144,14 @@ std::size_t DisplayBufferSize(const DisplayBuffer& buffer) { throw std::runtime_error(message); } require(!halfFloat || buffer.dccAddress == 0, "VideoOut: DCC metadata on a 16-bit float display buffer is not implemented"); - require(buffer.alignment == 65536u || (buffer.alignment == 4096u && buffer.tilingMode == 1), "VideoOut: 4 KiB display buffer alignment is only supported for linear buffers"); + require(buffer.alignment == 65536u || (buffer.alignment == 4096u && (buffer.tilingMode == 1 || buffer.tilingMode == DisplayTilingGcn)), "VideoOut: 4 KiB display buffer alignment is only supported for linear and PS4 buffers"); require(buffer.address != 0 && (buffer.address & (buffer.alignment - 1u)) == 0, buffer.alignment == 65536u ? "VideoOut: display buffer requires 64 KiB alignment" : "VideoOut: display buffer requires 4 KiB alignment"); - require(buffer.tilingMode <= 1, "VideoOut: unsupported display tiling mode"); + require(buffer.tilingMode <= 1 || buffer.tilingMode == DisplayTilingGcn, "VideoOut: unsupported display tiling mode"); + if (buffer.tilingMode == DisplayTilingGcn) { + require(buffer.dccAddress == 0 && !DisplayHalfFloat(buffer.pixelFormat), "VideoOut: PS4 display buffers carry no DCC and no half-float texels"); + require(buffer.pitchInPixel >= buffer.width && buffer.pitchInPixel % 128u == 0, "VideoOut: PS4 tiled display pitch is not padded to 128 pixels"); + return gcnLayout(buffer, DisplayTexelBytes(buffer.pixelFormat)).Bytes(); + } require(buffer.tilingMode != 0 || buffer.pitchInPixel == 0, "VideoOut: tiled display pitch is unsupported"); require(buffer.dccAddress == 0 || buffer.tilingMode == 0, "VideoOut: a linear display buffer cannot carry DCC metadata"); require(buffer.dccAddress != 0 || buffer.dccClearColor == 0, "VideoOut: a DCC clear color needs DCC metadata"); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp index 3fb5fc8217..11b89fe3d7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -117,7 +117,7 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const DrawProgram result{ {stage, address, std::span(snapshot.code).subspan(codeOffset), snapshot.headerAddress, snapshot.header}, userDataBase, - stage == Stage::Fragment ? 0u : 8u, + stage == Stage::Fragment || userDataBase == 0x04cu ? 0u : 8u, {}, {{{snapshot.codeAddress, std::as_bytes(std::span(snapshot.code))}, {snapshot.headerAddress, snapshot.header}}}, program, @@ -167,6 +167,8 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const append(0xc8, type, Stage::Mesh, 0x8b, 0x8c, Role::Main); InitializeMerged(queue, programs.back(), 0x82, type == 4, staticAbi); if (type == 4) append(0x88, 6, Stage::Mesh, 0x8b, 0x8c, Role::GeometryBack); + } else if (graphics.stages.legacyVertex) { + append(0x48, 2, Stage::Vertex, 0x4b, 0x4c, Role::Main); } else { append(0xc8, 2, Stage::Vertex, 0x8b, 0x8c, Role::Main); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index a30e3b7542..8ec6a2e600 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -12,7 +12,7 @@ namespace AgcDriver::DriverDetail { namespace { bool runtimeRegister(std::uint32_t offset) { - return offset - 0x00cu < 32u || offset - 0x08cu < 32u || offset - 0x10cu < 32u || offset == 0x082u || offset == 0x083u || offset == 0x102u || offset == 0x103u; + return offset - 0x00cu < 32u || offset - 0x04cu < 16u || offset - 0x08cu < 32u || offset - 0x10cu < 32u || offset == 0x082u || offset == 0x083u || offset == 0x102u || offset == 0x103u; } const auto& registerMasks() { @@ -71,10 +71,11 @@ std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords, bool mixPart(part, it->second); } }; - for (const auto base : {0x00cu, 0x08cu, 0x10cu}) { + for (const auto base : {0x00cu, 0x04cu, 0x08cu, 0x10cu}) { + const auto limit = base == 0x04cu ? 16u : 32u; const auto resources = queue.shader.find(base - 1); const auto count = resources == queue.shader.end() ? 0u : ((resources->second >> 1u) & 0x1fu) | (((resources->second >> 27u) & 1u) << 5u); - words(base, allUserWords ? 32u : std::min(count, 32u), userData); + words(base, allUserWords ? limit : std::min(count, limit), userData); } words(0x082u, 2, auxiliary); words(0x102u, 2, auxiliary); @@ -86,7 +87,7 @@ std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords, bool std::uint64_t DrawShaderSetKey(const QueueState& queue, const ShaderRegistry& registry) { std::uint64_t key = 0xcbf29ce484222325ull; const auto mix = [&](std::uint64_t value) { key = (key ^ value) * 0x100000001b3ull; }; - for (const auto base : {0x008u, 0x088u, 0x0c8u, 0x108u, 0x148u}) { + for (const auto base : {0x008u, 0x048u, 0x088u, 0x0c8u, 0x108u, 0x148u}) { const auto low = queue.shader.find(base); const auto high = queue.shader.find(base + 1); if (low == queue.shader.end() || high == queue.shader.end()) { mix(0); continue; } @@ -167,7 +168,7 @@ std::uint64_t Driver::drawRegisterKey(const QueueState& queue, const ShaderRegis (*components)[4] = DrawShaderSetKey(queue, registry); } mix(deviceSerial); - for (const auto base : {0x008u, 0x088u, 0x0c8u, 0x108u, 0x148u}) { + for (const auto base : {0x008u, 0x048u, 0x088u, 0x0c8u, 0x108u, 0x148u}) { const auto low = queue.shader.find(base); const auto high = queue.shader.find(base + 1); if (low == queue.shader.end() || high == queue.shader.end()) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp index fbe526d30a..5d910c8ed6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp @@ -7,6 +7,7 @@ #include "prx/libkernel/Time/include/SleepWake.hpp" #include #include +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" namespace AgcDriver { @@ -91,6 +92,15 @@ extern "C" void AgcDriverWaitIdle_nid_postfix() try { LibcAwaitExit_nid_postfix(); } +extern "C" void AgcDriverSubmitGraphics_nid_postfix(std::uint32_t* commands, std::uint32_t dwords) { + const Packet packet{commands, dwords, 0, {}}; + AgcDriver::Submit(&packet, 0); +} + +extern "C" void AgcDriverUseGcnShaders_nid_postfix() { + ShaderRecompiler::SetGcnGuestShaders(true); +} + static std::mutex& VulkanLoaderMutex() { static std::mutex mutex; return mutex; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp index bd1b6112de..d1c8772669 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp @@ -51,7 +51,7 @@ bool Driver::matchesFillKernel(std::span code, const std::v static const bool enabled = std::getenv("APS5_NO_FILL_HLE") == nullptr; static const bool patternEnabled = std::getenv("APS5_NO_PATTERN_FILL_HLE") == nullptr; if (!enabled || userData.size() < 8 || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1) return false; - if (patternEnabled && MatchesPatternFillKernel(code, userData, compute)) return true; + if (patternEnabled && (MatchesPatternFillKernel(code, userData, compute) || MatchesGcnPatternFillKernel(code, userData, compute))) return true; static constexpr std::array fillKernel{0xd7460004u, 0x04010c08u, 0x7e000204u, 0x7e020205u, 0x7e040206u, 0x7e060207u, 0xe01c2000u, 0x80000004u, 0xbf810000u}; if (!matchesBufferPatternFillKernel(code) && (code.size() < fillKernel.size() || !std::equal(fillKernel.begin(), fillKernel.end(), code.begin()))) return false; @@ -275,8 +275,8 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::spanaddress; bytes = memoryFill->bytes; - } else if (MatchesPatternFillKernel(code, userData, compute)) { - const auto range = DecodePatternFillRange(userData, packet); + } else if (MatchesPatternFillKernel(code, userData, compute) || MatchesGcnPatternFillKernel(code, userData, compute)) { + const auto range = MatchesGcnPatternFillKernel(code, userData, compute) ? DecodeGcnPatternFillRange(userData, packet) : DecodePatternFillRange(userData, packet); if (!range) return false; if (range->invocations == 0) return true; drainPendingInputWrites(queueId, {{range->control, 8}, {range->source, 4}}); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 069a5e418f..7123af76f0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1028,7 +1028,8 @@ std::vector ReadTableCallees(const ShaderRecompil } ShaderRecompiler::LinkedCalls LinkUserDataCallees(const ShaderRecompiler::RecompileRequest& request) { - if (request.shader.stage != ShaderRecompiler::ShaderStage::Compute) return {}; + const bool gcnVertex = ShaderRecompiler::GcnGuestShaders() && request.shader.stage == ShaderRecompiler::ShaderStage::Vertex; + if (request.shader.stage != ShaderRecompiler::ShaderStage::Compute && !gcnVertex) return {}; const auto code = request.shader.code; if (std::none_of(code.begin(), code.end(), [](std::uint32_t word) { return (word & 0xff80ff00u) == 0xbe802100u; })) return {}; const auto program = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(code); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 4662931be9..86713340d2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -3295,7 +3295,9 @@ void WriteFrameBmp(int index, std::uint32_t width, std::uint32_t height, std::sp ResidentPresent ResidentPresentable(const Graphics::Context& context, const Graphics::StorageTexture& image, const DisplayBuffer& buffer, VkFilter& filter, std::uint32_t& layer) { const auto& descriptor = image.Descriptor(); - if (descriptor.width != buffer.width || descriptor.height != buffer.height || descriptor.mipCount != 1 || image.ImageDepth() != 1) return ResidentPresent::None; + const bool gcn = buffer.tilingMode == DisplayTilingGcn; + if (gcn && (descriptor.tileMode != Graphics::TextureTileMode::kGcnDisplay2D || descriptor.width != buffer.pitchInPixel || descriptor.height < buffer.height || descriptor.mipCount != 1 || image.ImageDepth() != 1)) return ResidentPresent::None; + if (!gcn && (descriptor.width != buffer.width || descriptor.height != buffer.height || descriptor.mipCount != 1 || image.ImageDepth() != 1)) return ResidentPresent::None; const auto slice = ResidentPresentSlice(descriptor.baseAddress, image.GuestBytes(), image.ImageLayers(), buffer.address, DisplayBufferSize(buffer)); if (!slice) return ResidentPresent::None; layer = *slice; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp index f8acde495e..984e3f8a38 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include #include #include #include @@ -20,9 +21,17 @@ enum class ColorTileMode : std::uint32_t { D4KBX = 0x16, Z64KBX = 0x18, D64KBX = 0x1a, - RenderTarget = 0x1b + RenderTarget = 0x1b, + GcnThin1D = 0x100, + GcnDisplay1D = 0x101, + GcnDisplay2D = 0x102, + GcnThin2D = 0x103 }; +constexpr bool ColorTileModeIsGcn(ColorTileMode mode) { + return mode == ColorTileMode::GcnThin1D || mode == ColorTileMode::GcnDisplay1D || mode == ColorTileMode::GcnDisplay2D || mode == ColorTileMode::GcnThin2D; +} + constexpr bool ColorTileModeIsXor(ColorTileMode mode) { return mode == ColorTileMode::S4KBX || mode == ColorTileMode::D4KBX || mode == ColorTileMode::Z64KBX || mode == ColorTileMode::D64KBX || mode == ColorTileMode::RenderTarget; } @@ -32,6 +41,10 @@ constexpr std::uint32_t ColorTileModeBlockBytes(ColorTileMode mode) { } constexpr TextureTileMode ColorTextureTileMode(ColorTileMode mode) { + if (mode == ColorTileMode::GcnThin1D) return TextureTileMode::kGcnThin1D; + if (mode == ColorTileMode::GcnDisplay1D) return TextureTileMode::kGcnDisplay1D; + if (mode == ColorTileMode::GcnDisplay2D) return TextureTileMode::kGcnDisplay2D; + if (mode == ColorTileMode::GcnThin2D) return TextureTileMode::kGcnThin2D; return mode == ColorTileMode::Linear ? TextureTileMode::kLinear : mode == ColorTileMode::Standard4KB ? TextureTileMode::kStandard4KB : mode == ColorTileMode::Standard64KB ? TextureTileMode::kStandard64KB : mode == ColorTileMode::S4KBX ? TextureTileMode::kS4KBX : mode == ColorTileMode::D4KBX ? TextureTileMode::kD4KBX : mode == ColorTileMode::D64KBX ? TextureTileMode::kD64KBX : mode == ColorTileMode::Z64KBX ? TextureTileMode::kZ64KBX : TextureTileMode::kR64KBX; } @@ -43,7 +56,7 @@ public: ColorTargetLayout(std::uint32_t width, std::uint32_t height, ColorTileMode mode, std::uint32_t bytesPerElement = 4, std::uint32_t pipeBankXor = 0); std::size_t Bytes() const { return bytes; } std::size_t LinearBytes() const { return static_cast(width) * height * elementBytes; } - std::size_t Alignment() const { return ColorTileModeBlockBytes(mode); } + std::size_t Alignment() const { return blockBytes; } std::uint32_t BlocksPerRow() const { return pitch / blockWidth; } std::size_t Offset(std::uint32_t x, std::uint32_t y) const; void Detile(std::span source, std::span destination) const; @@ -59,7 +72,10 @@ private: std::uint32_t elementBytes; std::uint32_t blockWidth = 1; std::uint32_t blockHeight = 1; + std::size_t blockBytes = 0; std::uint32_t pipeBankXor = 0; + bool gcn = false; + std::array gcnEquation{}; // The XOR swizzle is linear over GF(2), so a block offset is xOffsets[x] ^ yOffsets[y]. const std::uint32_t* xOffsets = nullptr; const std::uint32_t* yOffsets = nullptr; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp index 17b5f7cb3b..d69fcb7dc6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp @@ -25,6 +25,21 @@ enum class TextureTileMode { kD64KBT, kS4KBX, kD4KBX, + kGcnThin1D, + kGcnDisplay1D, + kGcnDisplay2D, + kGcnThin2D, + kGcnDepth2D64, + kGcnDepth2D128, + kGcnDepth2D256, + kGcnDepth2D512, + kGcnDepth2D1K, + kGcnDepthPrt256, + kGcnDepthPrt1K, + kGcnDisplayPrt, + kGcnDisplay2DPrt, + kGcnThinPrt, + kGcnThin2DPrt, // Upstream's name for SW_64KB_R_X (tile mode 0x1b). RenderTarget64KB = kR64KBX }; @@ -91,8 +106,37 @@ struct GuestTextureResource { std::uint32_t allocatedMipCount = 0; std::uint32_t samples = 1; std::uint64_t fmaskCmaskAddress = 0; + std::uint32_t gcnPitch = 0; + bool gcnPow2Pad = false; + bool gcnLinearGeneral = false; }; +constexpr bool IsGcnMicroTiled(TextureTileMode mode) { + return mode == TextureTileMode::kGcnThin1D || mode == TextureTileMode::kGcnDisplay1D; +} + +constexpr bool IsGcnDepthTiled(TextureTileMode mode) { + return mode == TextureTileMode::kGcnDepth2D64 || mode == TextureTileMode::kGcnDepth2D128 || mode == TextureTileMode::kGcnDepth2D256 || mode == TextureTileMode::kGcnDepth2D512 || mode == TextureTileMode::kGcnDepth2D1K + || mode == TextureTileMode::kGcnDepthPrt256 || mode == TextureTileMode::kGcnDepthPrt1K; +} + +constexpr bool IsGcnPrtTiled(TextureTileMode mode) { + return mode == TextureTileMode::kGcnDepthPrt256 || mode == TextureTileMode::kGcnDepthPrt1K || mode == TextureTileMode::kGcnDisplayPrt || mode == TextureTileMode::kGcnDisplay2DPrt || mode == TextureTileMode::kGcnThinPrt || mode == TextureTileMode::kGcnThin2DPrt; +} + +constexpr bool IsGcnMacroTiled(TextureTileMode mode) { + return mode == TextureTileMode::kGcnDisplay2D || mode == TextureTileMode::kGcnThin2D || IsGcnDepthTiled(mode) || IsGcnPrtTiled(mode); +} + +constexpr bool IsGcnTiled(TextureTileMode mode) { + return IsGcnMicroTiled(mode) || IsGcnMacroTiled(mode); +} + +constexpr TextureTileMode GcnMicroTileMode(TextureTileMode mode) { + if (mode == TextureTileMode::kGcnDisplay2D || mode == TextureTileMode::kGcnDisplayPrt || mode == TextureTileMode::kGcnDisplay2DPrt) return TextureTileMode::kGcnDisplay1D; + return IsGcnMacroTiled(mode) ? TextureTileMode::kGcnThin1D : mode; +} + float EffectiveMinLod(const GuestTextureResource& resource); bool IsNullTextureResource(std::span words); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 190dd06e84..fcbca6dc45 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -42,6 +42,7 @@ struct ShaderStages { float maxTessellationLevel = 64.0f; std::uint32_t hullWaveSize = 64; std::optional tessellationGeometry; + bool legacyVertex = false; }; struct ColorTarget { @@ -305,7 +306,7 @@ struct DrawKeyRange { std::uint32_t first; std::uint32_t count; }; -inline constexpr std::array DrawKeyRegisters{{ +inline constexpr std::array DrawKeyRegisters{{ {RegisterBank::Context, 0x000, 1}, {RegisterBank::Context, 0x002, 2}, {RegisterBank::Context, 0x005, 1}, {RegisterBank::Context, 0x007, 7}, {RegisterBank::Context, 0x010, 6}, {RegisterBank::Context, 0x01a, 5}, {RegisterBank::Context, 0x080, 4}, {RegisterBank::Context, 0x08c, 4}, {RegisterBank::Context, 0x090, 2}, {RegisterBank::Context, 0x094, 2}, {RegisterBank::Context, 0x0b4, 2}, {RegisterBank::Context, 0x105, 4}, {RegisterBank::Context, 0x10b, 3}, {RegisterBank::Context, 0x10f, 6}, // SPI_PS_INPUT_CNTL_0..31, SPI_PS_INPUT_ENA/ADDR, SPI_PS_IN_CONTROL .. SPI_BARYC_CNTL, SPI_SHADER_POS/Z/COL_FORMAT, @@ -313,13 +314,13 @@ inline constexpr std::array DrawKeyRegisters{{ {RegisterBank::Context, 0x191, 32}, {RegisterBank::Context, 0x1b3, 2}, {RegisterBank::Context, 0x1b6, 3}, {RegisterBank::Context, 0x1c3, 3}, {RegisterBank::Context, 0x1e0, 8}, {RegisterBank::Context, 0x1ff, 1}, // DB_DEPTH_CONTROL .. PA_CL_VS_OUT_CNTL, PA_SC_MODE_CNTL_0/1, VGT_GS_MODE, VGT_GS_VERT_ITEMSIZE, // PA_SU_VTX_CNTL, the sample masks, PA_SC_CONSERVATIVE_RASTERIZATION_CNTL. - {RegisterBank::Context, 0x200, 8}, {RegisterBank::Context, 0x286, 2}, {RegisterBank::Context, 0x292, 2}, {RegisterBank::Context, 0x29b, 1}, {RegisterBank::Context, 0x2ab, 1}, {RegisterBank::Context, 0x2af, 1}, {RegisterBank::Context, 0x2ce, 1}, {RegisterBank::Context, 0x2d5, 2}, {RegisterBank::Context, 0x2db, 2}, {RegisterBank::Context, 0x2de, 6}, {RegisterBank::Context, 0x2f8, 2}, {RegisterBank::Context, 0x2fe, 16}, {RegisterBank::Context, 0x30e, 2}, {RegisterBank::Context, 0x313, 1}, + {RegisterBank::Context, 0x200, 8}, {RegisterBank::Context, 0x286, 2}, {RegisterBank::Context, 0x292, 2}, {RegisterBank::Context, 0x29b, 1}, {RegisterBank::Context, 0x2a8, 2}, {RegisterBank::Context, 0x2ab, 1}, {RegisterBank::Context, 0x2af, 1}, {RegisterBank::Context, 0x2ce, 1}, {RegisterBank::Context, 0x2d5, 2}, {RegisterBank::Context, 0x2db, 2}, {RegisterBank::Context, 0x2de, 6}, {RegisterBank::Context, 0x2f8, 2}, {RegisterBank::Context, 0x2fe, 16}, {RegisterBank::Context, 0x30e, 2}, {RegisterBank::Context, 0x313, 1}, // CB_COLOR0..7_BASE .. DCC_BASE (15 words a slot), CB_COLOR0..7_BASE_EXT, DCC_BASE_EXT, ATTRIB2, ATTRIB3. {RegisterBank::Context, 0x318, 0x78}, {RegisterBank::Context, 0x390, 8}, {RegisterBank::Context, 0x398, 0x10}, {RegisterBank::Context, 0x3a8, 0x18}, // The pixel program address, RSRC2 and user words; the geometry-back user pointer and program // address; the vertex/geometry-front RSRC1/RSRC2 and user words; the vertex program address; // the hull user pointer, program address, RSRC2 and user words; the local program address. - {RegisterBank::Shader, 0x008, 0x24}, {RegisterBank::Shader, 0x082, 2}, {RegisterBank::Shader, 0x088, 2}, {RegisterBank::Shader, 0x08a, 0x22}, {RegisterBank::Shader, 0x0c8, 2}, {RegisterBank::Shader, 0x102, 2}, {RegisterBank::Shader, 0x108, 2}, {RegisterBank::Shader, 0x10b, 0x21}, {RegisterBank::Shader, 0x148, 2}, + {RegisterBank::Shader, 0x008, 0x24}, {RegisterBank::Shader, 0x048, 0x14}, {RegisterBank::Shader, 0x082, 2}, {RegisterBank::Shader, 0x088, 2}, {RegisterBank::Shader, 0x08a, 0x22}, {RegisterBank::Shader, 0x0c8, 2}, {RegisterBank::Shader, 0x102, 2}, {RegisterBank::Shader, 0x108, 2}, {RegisterBank::Shader, 0x10b, 0x21}, {RegisterBank::Shader, 0x148, 2}, // GE_PRIM_TYPE, GE_MULTI_PRIM_IB_RESET_EN, the geometry subgroup sizes. {RegisterBank::UserConfig, 0x242, 1}, {RegisterBank::UserConfig, 0x24b, 1}, {RegisterBank::UserConfig, 0x25b, 1}, }}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 952b359a32..dfc8e9679e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -163,6 +163,7 @@ private: VkFormat blockFormat = VK_FORMAT_UNDEFINED; bool repackCopy = false; std::uint32_t copyWidth = 0, copyHeight = 0, copyLevels = 0, copyLayers = 0, copyElementBytes = 0; + std::uint32_t copyTexelsPerElement = 4; }; VkFormat AttachmentProxyFormat(const Context& context, VkFormat format); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp index dafa52d5f5..8b27373458 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp @@ -22,6 +22,8 @@ struct TileMipLayout { bool tail; std::uint32_t tailX; std::uint32_t tailY; + bool gcnMicroTiled = false; + std::uint32_t gcnSplitStride = 0; }; std::vector ComputeMipLayout(TextureTileMode tileMode, std::uint32_t format, std::uint32_t width, std::uint32_t height, std::uint32_t mipCount); @@ -67,6 +69,10 @@ struct SurfaceGeometry { std::uint32_t blockDepth = 1; std::uint64_t layerBytes = 0; + bool levelMajor = false; + std::uint64_t GuestOffset(std::uint32_t layer, const TileMipLayout& mip) const { + return levelMajor ? mip.tiledOffset + static_cast(layer) * mip.tiledSize : GuestLayerOffset(layer) + mip.tiledOffset; + } std::uint64_t GuestLayerOffset(std::uint32_t layer) const { return thick ? static_cast(layer / blockDepth) * layerBytes : static_cast(layer) * layerBytes; } std::uint64_t LinearLayerOffset(std::uint32_t layer) const { return static_cast(layer) * sliceLinearBytes; } std::uint32_t CopyLayer(std::uint32_t layer) const { return imageDepth > 1 ? 0u : layer; } @@ -74,6 +80,17 @@ struct SurfaceGeometry { bool HasLayer(std::uint32_t level, std::uint32_t layer) const { return imageDepth <= 1 || layer < std::max(imageDepth >> level, 1u); } }; SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor); +std::vector ComputeGcnMipLayout(TextureTileMode tileMode, std::uint32_t format, std::uint32_t width, std::uint32_t height, std::uint32_t pitch, bool pow2Pad, std::uint32_t mipCount, bool linearGeneral = false, std::uint32_t layers = 1); +struct GcnTileGeometry { + std::uint32_t blockBytes; + std::uint32_t blockWidth; + std::uint32_t blockHeight; + std::array equation; + std::uint32_t splitSlices = 1; + std::array splitMasks{}; +}; +GcnTileGeometry GcnTiling(TextureTileMode tileMode, std::uint32_t bytesPerElement); +inline TextureTileMode MipTileMode(TextureTileMode tileMode, const TileMipLayout& mip) { return mip.gcnMicroTiled ? GcnMicroTileMode(tileMode) : tileMode; } bool LevelsFitAllocation(const GuestTextureResource& surface, std::uint32_t levels); GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed, bool keysReadAsStored); GuestTextureResource ArrayLayerWindow(const GuestTextureResource& viewed, std::uint32_t maxArrayLayers); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/TextureDetile.comp b/core/libs/prx/libSceAgcDriver/Graphics/shaders/TextureDetile.comp index fcff04497b..6ebceb5667 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/shaders/TextureDetile.comp +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/TextureDetile.comp @@ -29,6 +29,10 @@ layout(constant_id = 19) const uint EQ15 = 0u; // Nonzero: block extent in elements (thick 3D blocks are not the thin 2D shape derived below). layout(constant_id = 20) const uint BLOCK_WIDTH = 0u; layout(constant_id = 21) const uint BLOCK_HEIGHT = 0u; +layout(constant_id = 22) const uint SPLIT0 = 0u; +layout(constant_id = 23) const uint SPLIT1 = 0u; +layout(constant_id = 24) const uint SPLIT2 = 0u; +layout(constant_id = 25) const uint SPLIT3 = 0u; layout(local_size_x = 8, local_size_y = 8) in; @@ -64,6 +68,7 @@ layout(push_constant) uniform Push { uint columnBegin; uint rowBegin; uint pipeBankXor; + uint splitStride; } params; uint equationBit(uint mask, uvec2 p) { @@ -242,6 +247,10 @@ void main() { uint blockIndex = block.y * params.blocksPerRow + block.x; uint tiledOffset = blockIndex * BLOCK_BYTES + blockOffset(swizzle); + if (SPLIT0 != 0u) { + uint split = equationBit(SPLIT0, p) | (equationBit(SPLIT1, p) << 1) | (equationBit(SPLIT2, p) << 2) | (equationBit(SPLIT3, p) << 3); + tiledOffset += split * params.splitStride; + } if (tiledOffset < params.rangeBegin || tiledOffset >= params.rangeEnd) { return; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp index 6d1f9ec2fe..87def748a7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp @@ -54,6 +54,7 @@ ColorTileMode DecodeColorTileMode(std::uint32_t attrib3) { } TileMipLayout ComputeColorTargetMipLayout(ColorTileMode mode, std::uint32_t bytesPerElement, std::uint32_t surfaceWidth, std::uint32_t surfaceHeight, std::uint32_t mipCount, std::uint32_t mipLevel) { + require(!ColorTileModeIsGcn(mode), "AGC graphics: PS4 color target mips are laid out by ComputeGcnMipLayout"); auto mips = ComputeElementMipLayout(ColorTextureTileMode(mode), bytesPerElement, surfaceWidth, surfaceHeight, mipCount); require(mipLevel < mips.size(), "AGC graphics: color target mip is outside its surface"); auto mip = mips[mipLevel]; @@ -156,8 +157,22 @@ ColorTargetLayout::ColorTargetLayout(std::uint32_t width, std::uint32_t height, yOffsets = tables.y.data(); break; } + case ColorTileMode::GcnThin1D: + case ColorTileMode::GcnDisplay1D: + case ColorTileMode::GcnDisplay2D: + case ColorTileMode::GcnThin2D: { + const auto geometry = GcnTiling(ColorTextureTileMode(mode), bytesPerElement); + blockWidth = geometry.blockWidth; + blockHeight = geometry.blockHeight; + pitch = (width + blockWidth - 1u) / blockWidth * blockWidth; + paddedHeight = (height + blockHeight - 1u) / blockHeight * blockHeight; + gcn = true; + gcnEquation = geometry.equation; + break; + } default: throw std::runtime_error("AGC graphics: unsupported color tile mode"); } + blockBytes = gcn ? static_cast(blockWidth) * blockHeight * bytesPerElement : ColorTileModeBlockBytes(mode); const auto size = static_cast(pitch) * paddedHeight * bytesPerElement; require(size <= std::numeric_limits::max(), "AGC graphics: color surface size overflow"); bytes = static_cast(size); @@ -166,6 +181,11 @@ ColorTargetLayout::ColorTargetLayout(std::uint32_t width, std::uint32_t height, std::size_t ColorTargetLayout::offset(std::uint32_t x, std::uint32_t y) const { if (mode == ColorTileMode::Linear) return (static_cast(y) * pitch + x) * elementBytes; const auto block = static_cast(y / blockHeight) * (pitch / blockWidth) + x / blockWidth; + if (gcn) { + std::uint32_t inBlock = 0; + for (std::uint32_t bit = 0; bit < 16u; ++bit) inBlock |= parity((x & gcnEquation[bit] & 0xfffu) ^ ((y << 12u) & gcnEquation[bit] & 0xfff000u)) << bit; + return block * blockBytes + inBlock; + } auto inBlock = xOffsets[x % blockWidth] ^ yOffsets[y % blockHeight]; if (equation != nullptr) { const auto highX = (x & ~(blockWidth - 1u)) & 0xfffu; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index d8af46c18b..34d06b7824 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -1299,6 +1299,7 @@ public: Require(requested != 0 && requested <= layers, "depth plane copy slices exceed its layers"); Require(baseLayer < requested, "a depth plane copy view starts past the slices it was given"); const auto geometry = DescribeSurface(resource); + Require(!geometry.levelMajor, "depth planes of a PS4 layered surface with several mip levels (stored level by level) are not implemented"); const bool guestFill = std::any_of(slices.begin(), slices.end(), [](const auto& slice) { return slice.image == VK_NULL_HANDLE; }); if (guestFill && (geometry.layers != requested || geometry.sliceLinearBytes < sliceBytes() || geometry.mips.empty())) { char text[320]; @@ -2510,6 +2511,7 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::span slices; const auto geometry = DescribeSurface(resource); + Require(!geometry.levelMajor, "depth planes of a PS4 layered surface with several mip levels (stored level by level) are not implemented"); for (std::uint32_t layer = 0; layer < layers; ++layer) { const auto address = base.address + geometry.GuestLayerOffset(layer); auto* slice = depthSlice(context.device, address, base); @@ -2646,6 +2648,7 @@ std::shared_ptr layeredDepthStorage(const Context& context, Dept const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; const auto bytesPerTexel = d16 ? 2u : 4u; surface.RequireStorable(resource); + Require(!DescribeSurface(resource).levelMajor, "depth planes of a PS4 layered surface with several mip levels (stored level by level) are not implemented"); auto& entry = layeredStorages()[{context.device, resource.baseAddress, resource.width, resource.height, layers, resource.format, static_cast(resource.tileMode), static_cast(resource.dimension), resource.baseArray}]; if (entry == nullptr) entry = std::make_unique(context, resource, target, bytesPerTexel, resource.baseArray, layers); else static_cast(entry->storage->Refresh()); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 0f22b8c15b..7e47d60a47 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -4,6 +4,8 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" +#include #include #include #include @@ -40,6 +42,28 @@ TextureTileMode resolveTileMode(std::uint32_t raw) { } } +TextureTileMode resolveGcnTileMode(std::uint32_t index) { + switch (index) { + case 0u: return TextureTileMode::kGcnDepth2D64; + case 1u: return TextureTileMode::kGcnDepth2D128; + case 2u: return TextureTileMode::kGcnDepth2D256; + case 3u: return TextureTileMode::kGcnDepth2D512; + case 4u: return TextureTileMode::kGcnDepth2D1K; + case 6u: return TextureTileMode::kGcnDepthPrt256; + case 7u: return TextureTileMode::kGcnDepthPrt1K; + case 8u: case 31u: return TextureTileMode::kLinear; + case 11u: return TextureTileMode::kGcnDisplayPrt; + case 12u: return TextureTileMode::kGcnDisplay2DPrt; + case 16u: return TextureTileMode::kGcnThinPrt; + case 17u: return TextureTileMode::kGcnThin2DPrt; + case 5u: case 13u: return TextureTileMode::kGcnThin1D; + case 9u: return TextureTileMode::kGcnDisplay1D; + case 10u: return TextureTileMode::kGcnDisplay2D; + case 14u: return TextureTileMode::kGcnThin2D; + default: throw std::runtime_error("AGC graphics: PS4 texture tile mode index " + std::to_string(index) + " (3D or thick) is not implemented"); + } +} + TextureDimension resolveDimension(std::uint32_t raw) { switch (raw) { case 8: return TextureDimension::k1D; @@ -101,8 +125,14 @@ bool IsNullTextureResource(std::span words) { return base40 == 0 || type <= 1; } -GuestTextureResource DecodeTextureResource(std::span words) { - Require(words.size() == 8, "guest texture descriptor must contain 8 dwords"); +GuestTextureResource DecodeTextureResource(std::span descriptor) { + Require(descriptor.size() == 8, "guest texture descriptor must contain 8 dwords"); + const bool gcn = ShaderRecompiler::GcnGuestShaders(); + std::array local{}; + std::copy(descriptor.begin(), descriptor.end(), local.begin()); + const auto gcnTiling = gcn ? local[7] : 0u; + if (gcn) local[6] = local[7] = 0u; + const std::span words(local); const auto base40 = (static_cast(words[0]) | (static_cast(words[1]) << 32u)) & 0xffffffffffull; auto baseAddress = base40 << 8u; @@ -191,11 +221,11 @@ GuestTextureResource DecodeTextureResource(std::span words) for (const auto selector : {dstSelX, dstSelY, dstSelZ, dstSelW}) Require(identity || selector == 0u || selector == 1u || selector == 4u, "FMASK destination channel selector is not implemented"); } - const auto describe = [&] { return " (format " + std::to_string(format) + ", " + std::to_string(width) + "x" + std::to_string(height) + "x" + std::to_string(depth + 1) + ", type " + std::to_string(typeRaw) + ", tile " + std::to_string(tileModeRaw) + ", mips " + std::to_string(baseLevel) + ".." + std::to_string(lastLevel) + " of " + std::to_string(maxMip + 1) + ")"; }; + const auto describe = [&] { return " (format " + std::to_string(format) + ", " + std::to_string(width) + "x" + std::to_string(height) + "x" + std::to_string(depth + 1) + ", type " + std::to_string(typeRaw) + (gcn ? ", PS4 tile index " + std::to_string(ShaderRecompiler::GcnImageTilingIndex(gcnTiling)) : ", tile " + std::to_string(tileModeRaw)) + ", mips " + std::to_string(baseLevel) + ".." + std::to_string(lastLevel) + " of " + std::to_string(maxMip + 1) + ")"; }; TextureTileMode tileMode; TextureDimension dimension; try { - tileMode = resolveTileMode(tileModeRaw); + tileMode = gcn ? resolveGcnTileMode(ShaderRecompiler::GcnImageTilingIndex(gcnTiling)) : resolveTileMode(tileModeRaw); dimension = resolveDimension(typeRaw); } catch (const std::exception& error) { throw std::runtime_error(std::string(error.what()) + describe()); @@ -278,6 +308,12 @@ GuestTextureResource DecodeTextureResource(std::span words) result.iterate256 = iterate256; result.minLod = minLod; result.samples = samples; + if (gcn) { + result.gcnPitch = ShaderRecompiler::GcnImagePitch(gcnTiling); + result.gcnPow2Pad = ShaderRecompiler::GcnImagePow2Pad(gcnTiling); + result.gcnLinearGeneral = ShaderRecompiler::GcnImageTilingIndex(gcnTiling) == 31u; + Require(result.gcnPitch >= width, "PS4 texture descriptor has a pitch below its width" + describe()); + } if (baseLevel > maxMip) { const bool fits = LevelsFitAllocation(result, lastLevel + 1u); if (!fits && ZeroTail(words)) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index c4fb6bad61..65c3464c15 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -10,6 +10,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libc/include/General.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include #include #include @@ -449,8 +450,9 @@ DepthTarget decodeDepthPlane(const Registers& cx) { }; DepthTarget depth{}; depth.samples = 1u << ((read(cx, 0x010) >> 2u) & 3u); - depth.depthSwizzle = (read(cx, 0x010) >> 4u) & 31u; - depth.iterate256 = (read(cx, 0x010) & (1u << 20u)) != 0; + const bool gcn = ShaderRecompiler::GcnGuestShaders(); + depth.depthSwizzle = gcn ? 0u : (read(cx, 0x010) >> 4u) & 31u; + depth.iterate256 = !gcn && (read(cx, 0x010) & (1u << 20u)) != 0; depth.firstSlice = view & 0x1fffu; depth.lastSlice = (view >> 13u) & 0x7ffu; depth.address = zFormat != 0 ? base(0x012, 0x01a) : 0; @@ -464,8 +466,14 @@ DepthTarget decodeDepthPlane(const Registers& cx) { depth.htileVrsEncoding = (surface->second >> 19u) & 3u; } } - const auto size = read(cx, 0x007); - depth.extent = {(size & 0x3fffu) + 1u, ((size >> 16u) & 0x3fffu) + 1u}; + if (gcn) { + const auto size = read(cx, 0x016); + depth.extent = {((size & 0x7ffu) + 1u) * 8u, (((size >> 11u) & 0x7ffu) + 1u) * 8u}; + } else { + const auto size = read(cx, 0x007); + depth.extent = {(size & 0x3fffu) + 1u, ((size >> 16u) & 0x3fffu) + 1u}; + } + Require(!gcn || (maxMip == 0 && viewMip == 0), "PS4 depth targets with mip levels are not implemented"); if (maxMip != 0) { char message[320]; std::snprintf(message, sizeof(message), "depth view mip %u of %u at %ux%u (DB_Z_INFO 0x%08x, DB_DEPTH_VIEW 0x%08x)", viewMip, maxMip + 1u, depth.extent.width, depth.extent.height, read(cx, 0x010), view); @@ -1290,7 +1298,16 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { const auto provokingVertex = raster != queue.context.end() && (raster->second & (1u << 19u)) != 0 ? outputSize - 1u : 0u; return ShaderRecompiler::MeshConfiguration{inputPrimitive, groupPrimitives, (groupPrimitives - 1u) * inputStep + inputSize, maxVertices, maxPrimitives, ((threads + result.vertexWaveSize - 1u) / result.vertexWaveSize) * result.vertexWaveSize, ((resources >> 19u) & 0xffu) * 128u, provokingVertex, esgsItemSize, outputSize}; }; - if (path == ShaderPath::Vertex) { + if (path == ShaderPath::Vertex && ShaderRecompiler::GcnGuestShaders() && (value & 0x2000u) == 0) { + validate(value == 0, "GCN vertex routing other than a plain VS is unsupported"); + const auto unitStepRate = [&](std::uint32_t offset) { + const auto rate = find(queue.context, offset); + return rate == queue.context.end() || rate->second == 1u; + }; + const auto components = (read(queue.shader, 0x4a, RegisterBank::Shader) >> 24u) & 3u; + validate(components == 0 || (unitStepRate(0x2a8) && (components == 1 || unitStepRate(0x2a9))), "GCN vertex instance step rates other than 1 are not implemented"); + result.legacyVertex = true; + } else if (path == ShaderPath::Vertex) { validate((value & 0x2000u) != 0, "legacy vertex routing without PRIMGEN_EN is unsupported"); validate((value & ~0x0247a010u) == 0 || value == 0x02002008u, "unsupported vertex routing, scheduling or wave-ID state"); } else if (path == ShaderPath::Tessellation || path == ShaderPath::TessellationGeometry) { @@ -2028,6 +2045,7 @@ GuestTextureResource SurfaceForTarget(const ColorTarget& color) { surface.dccAddress = color.dccAddress != 0 ? color.dccAddress : color.chainDccAddress; surface.dccAlphaOnMsb = color.dccAlphaOnMsb; surface.dccPipeAligned = color.dccPipeAligned; + if (ColorTileModeIsGcn(color.tileMode)) surface.gcnPitch = surface.width; return surface; } DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, std::uint32_t swap) { @@ -2037,7 +2055,54 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, } +ColorTarget DecodeGcnColorBuffer(const Registers& cx, std::uint32_t slot) { + const auto stride = slot * 0xfu; + ColorTarget color{}; + color.slot = slot; + const auto info = read(cx, 0x31c + stride); + const auto number = (info >> 8u) & 7u; + const auto swap = (info >> 11u) & 3u; + const auto decoded = DecodeColorFormat((info >> 2u) & 0x1fu, number, swap); + const auto attrib = read(cx, 0x31d + stride); + color.samples = 1u << ((attrib >> 12u) & 7u); + Require(color.samples == 1u, "multisampled GCN color targets are not implemented"); + Require((info & 0x3u) == 0 && (info & 0x80u) == 0, "endian-swapped or linear-general GCN color targets are not implemented"); + Require((info & 0x8000u) != 0 || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported"); + const auto view = read(cx, 0x31b + stride); + Require((view & 0x7ffu) == 0 && ((view >> 13u) & 0x7ffu) == 0, "GCN color array slices are not implemented"); + const auto pitch = ((read(cx, 0x319 + stride) & 0x7ffu) + 1u) * 8u; + const auto sliceElements = ((read(cx, 0x31a + stride) & 0x3fffffu) + 1u) * 64u; + color.extent = {pitch, sliceElements / pitch}; + color.surfaceExtent = color.extent; + color.elementBytes = decoded.elementBytes; + switch (attrib & 0x1fu) { + case 8u: color.tileMode = ColorTileMode::Linear; break; + case 5u: case 13u: color.tileMode = ColorTileMode::GcnThin1D; break; + case 9u: color.tileMode = ColorTileMode::GcnDisplay1D; break; + case 10u: color.tileMode = ColorTileMode::GcnDisplay2D; break; + case 14u: color.tileMode = ColorTileMode::GcnThin2D; break; + default: throw std::runtime_error("AGC graphics: PS4 color target tile mode index " + std::to_string(attrib & 0x1fu) + " is not implemented"); + } + color.mipCount = 1; + color.surfaceAddress = static_cast(read(cx, 0x318 + stride)) << 8u; + color.address = color.surfaceAddress; + const ColorTargetLayout layout(color.extent.width, color.extent.height, color.tileMode, color.elementBytes); + Require(layout.Bytes() == static_cast(pitch) * color.extent.height * color.elementBytes, "PS4 color target pitch or slice is not padded the way its tile mode pads them"); + color.bytes = layout.Bytes(); + color.format = decoded.format; + color.componentMapping = decoded.componentMapping; + color.packing = decoded.packing; + for (std::uint32_t word = 0; word < 2; ++word) { + const auto clear = find(cx, 0x323 + word + stride); + color.clearWords[word] = clear == cx.end() ? 0u : clear->second; + } + Require(((info >> 13u) & 1u) == 0, "PS4 color target fast clear (CMASK) is not implemented"); + GuestMemory::CheckRange(reinterpret_cast(color.address), color.bytes, layout.Alignment(), true); + return color; +} + ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allowMultisampleDcc) { + if (ShaderRecompiler::GcnGuestShaders()) return DecodeGcnColorBuffer(cx, slot); const auto stride = slot * 0xfu; ColorTarget color{}; color.slot = slot; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 53ad0992b4..488ed1285c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -453,12 +453,11 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { - const auto guestLayerOffset = geometry.GuestLayerOffset(layer); const auto linearLayerOffset = geometry.LinearLayerOffset(layer); for (std::uint32_t level = 0; level < mips.size(); ++level) { if (!geometry.HasLayer(level, layer)) continue; const auto& mip = mips[level]; - detiler.Dispatch(commands, descriptor.tileMode, elementBytes, tiled->Handle(), guestLayerOffset + mip.tiledOffset, linear->Handle(), linearLayerOffset + mip.linearOffset, mip, false, layer, geometry.thick, {.pipeBankXor = descriptor.pipeBankXor}); + detiler.Dispatch(commands, descriptor.tileMode, elementBytes, tiled->Handle(), geometry.GuestOffset(layer, mip), linear->Handle(), linearLayerOffset + mip.linearOffset, mip, false, layer, geometry.thick, {.pipeBankXor = descriptor.pipeBankXor}); } } if (IsRepackedTextureFormat(descriptor.format)) { @@ -615,6 +614,12 @@ bool Texture::CanDepthCopyFrom(const StorageTexture& source, const GuestTextureR bool Texture::CanBlockCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor) { const auto& from = source.Descriptor(); + if (descriptor.gcnPitch != 0 && from.gcnPitch == descriptor.gcnPitch && (descriptor.width != from.width || descriptor.height != from.height)) { + return !IsBlockCompressed(descriptor.format) && !IsBlockCompressed(from.format) && !IsSrgbTextureFormat(descriptor.format) && BytesPerElement(from.format) == BytesPerElement(descriptor.format) + && descriptor.baseAddress == from.baseAddress && descriptor.tileMode == from.tileMode && descriptor.dimension == TextureDimension::k2D && from.dimension == TextureDimension::k2D + && descriptor.mipCount == 1u && from.mipCount == 1u && descriptor.width <= from.width && descriptor.height <= from.height + && source.ImageDepth() == 1 && DescribeSurface(descriptor).guestBytes == source.GuestBytes(); + } if (!IsBlockCompressed(descriptor.format) || IsBlockCompressed(from.format) || BytesPerElement(from.format) != BytesPerElement(descriptor.format)) return false; if (descriptor.dimension != TextureDimension::k2D && descriptor.dimension != TextureDimension::k2DArray) return false; if (descriptor.baseAddress != from.baseAddress || descriptor.pipeBankXor != from.pipeBankXor || descriptor.dimension != from.dimension || descriptor.tileMode != from.tileMode || descriptor.depthOrLastArray != from.depthOrLastArray || descriptor.mipCount > from.mipCount) return false; @@ -701,6 +706,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& Require(source != nullptr && CanBlockCopyFrom(*source, descriptor), "storage image cannot serve a block copy of the sampled texture"); blockFormat = SampledTextureFormat(context, descriptor.format); const auto geometry = DescribeSurface(descriptor); + copyTexelsPerElement = IsBlockCompressed(descriptor.format) ? 4u : 1u; copyWidth = descriptor.width; copyHeight = descriptor.height; copyLevels = descriptor.mipCount; @@ -999,7 +1005,7 @@ bool Texture::refreshBlockCopy() { VkImageCopy region{}; region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, copyLayers}; region.dstSubresource = region.srcSubresource; - region.extent = {std::max(copyWidth >> level, 1u) / 4u, std::max(copyHeight >> level, 1u) / 4u, 1u}; + region.extent = {std::max(copyWidth >> level, 1u) / copyTexelsPerElement, std::max(copyHeight >> level, 1u) / copyTexelsPerElement, 1u}; regions.push_back(region); } auto* recorder = Recorder::Active(); @@ -1442,6 +1448,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, descriptor.depthOrLastArray, descriptor.baseArray, static_cast(descriptor.dimension), static_cast(descriptor.tileMode), descriptor.format, static_cast(vkFormat)); #endif geometry = DescribeSurface(descriptor); + Require(!geometry.levelMajor, "a storage image of a PS4 layered surface with several mip levels (stored level by level) is not implemented"); mips = geometry.mips; arrayLayers = geometry.layers; const auto elementBytes = BytesPerElement(descriptor.format); @@ -1452,7 +1459,8 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, const bool layered = geometry.layers >= 2 && !geometry.thick && geometry.imageDepth == 1 && geometry.layers == geometry.imageLayers && geometry.layerBytes * geometry.layers == guestBytes && geometry.layerBytes % 65536 == 0 && descriptor.baseAddress % 65536 == 0; // Tile blocks divide the stamp blocks, so at an aligned base every unit is whole tile blocks // of its (layer, mip) slices (sliceWindows). - blockUnits = LayerTrackingEnabled() && BlockTrackingEnabled() && !geometry.thick && descriptor.tileMode != TextureTileMode::kLinear && descriptor.baseAddress % 65536 == 0; + blockUnits = LayerTrackingEnabled() && BlockTrackingEnabled() && !geometry.thick && descriptor.tileMode != TextureTileMode::kLinear && descriptor.baseAddress % 65536 == 0 + && std::none_of(mips.begin(), mips.end(), [](const TileMipLayout& mip) { return mip.gcnSplitStride != 0; }); trackedLayers = blockUnits ? static_cast((guestBytes + 65535) / 65536) : layered && LayerTrackingEnabled() ? geometry.layers : 1u; trackedLayerBytes = blockUnits ? 65536 : guestBytes / trackedLayers; layerGeneration.assign(trackedLayers, 0); @@ -2977,12 +2985,12 @@ std::vector> StorageTexture::unitRuns(co std::vector StorageTexture::sliceWindows(std::span> runs) const { std::vector windows; const auto elementBytes = BytesPerElement(descriptor.format); - const auto block = ThinBlockLayout(descriptor.tileMode, elementBytes); for (const auto& [runBegin, runEnd] : runs) { for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { for (std::uint32_t level = 0; level < descriptor.mipCount; ++level) { if (!geometry.HasLayer(level, layer)) continue; const auto& mip = mips[level]; + const auto block = ThinBlockLayout(MipTileMode(descriptor.tileMode, mip), elementBytes); const auto sliceBegin = geometry.GuestLayerOffset(layer) + mip.tiledOffset; const auto sliceEnd = sliceBegin + mip.tiledSize; if (runEnd <= sliceBegin || sliceEnd <= runBegin) continue; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp index 344d80c594..b0a1e2588f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp @@ -37,6 +37,7 @@ struct Push { std::uint32_t columnBegin; std::uint32_t rowBegin; std::uint32_t pipeBankXor; + std::uint32_t splitStride; }; std::uint32_t BlockBytesFor(TextureTileMode tileMode) { @@ -148,8 +149,18 @@ VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elem } // Constants 0-3 select element size, block size, addressing family and direction; 4-19 carry the // per-bit XOR equation for the equation family (2); 20-21 override the block extent in elements. - std::array values{elementBytes, BlockBytesFor(tileMode), tileMode == TextureTileMode::kLinear ? 0u : 1u, retile ? 1u : 0u}; - if (thick) { + const bool gcn = IsGcnTiled(tileMode); + std::array values{elementBytes, gcn ? 0u : BlockBytesFor(tileMode), tileMode == TextureTileMode::kLinear ? 0u : 1u, retile ? 1u : 0u}; + if (gcn) { + Require(!thick, "PS4 tiled surfaces are detiled thin"); + const auto geometry = GcnTiling(tileMode, elementBytes); + values[1] = geometry.blockBytes; + values[2] = 2u; + std::copy(geometry.equation.begin(), geometry.equation.end(), values.begin() + 4); + values[20] = geometry.blockWidth; + values[21] = geometry.blockHeight; + std::copy(geometry.splitMasks.begin(), geometry.splitMasks.end(), values.begin() + 22); + } else if (thick) { const auto thickMode = tileMode == TextureTileMode::kStandard4KB ? 0x105u : tileMode == TextureTileMode::kS4KBX ? 0x115u : tileMode == TextureTileMode::kStandard64KB ? 0x109u : tileMode == TextureTileMode::kS64KBX ? 0x119u : tileMode == TextureTileMode::kD64KBX ? 0x11au : 0u; Require(thickMode != 0, "3D textures are only detiled thick from SW_4KB_S, SW_4KB_S_X, SW_64KB_S, SW_64KB_S_X or SW_64KB_D_X"); const auto* equation = FindTextureSwizzleEquation(thickMode, elementBytes); @@ -168,7 +179,7 @@ VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elem if (image && imageModule == VK_NULL_HANDLE) { imageModule = CreateShaderModule(context, TEXTURE_DETILE_IMAGE_SPV, sizeof(TEXTURE_DETILE_IMAGE_SPV), "vkCreateShaderModule"); } - std::array entries{}; + std::array entries{}; for (std::uint32_t index = 0; index < entries.size(); ++index) entries[index] = {index, index * 4u, 4}; VkSpecializationInfo specialization{}; specialization.mapEntryCount = static_cast(entries.size()); @@ -206,7 +217,7 @@ void TextureDetiler::Dispatch(VkCommandBuffer commands, TextureTileMode tileMode Require(source != VK_NULL_HANDLE && destination != VK_NULL_HANDLE, "texture detiling requires source and destination buffers"); std::uint32_t columnEnd = 0, rowEnd = 0; checkWindow(layout, window, columnEnd, rowEnd); - const auto target = pipeline(tileMode, elementBytes, retile, thick); + const auto target = pipeline(MipTileMode(tileMode, layout), elementBytes, retile, thick); const auto alignment = std::max(context.limits.minStorageBufferOffsetAlignment, 4); const auto sourceDescriptorOffset = sourceOffset - sourceOffset % alignment; const auto destinationDescriptorOffset = destinationOffset - destinationOffset % alignment; @@ -258,6 +269,7 @@ void TextureDetiler::Dispatch(VkCommandBuffer commands, TextureTileMode tileMode push.columnBegin = window.columnBegin; push.rowBegin = window.rowBegin; push.pipeBankXor = window.pipeBankXor; + push.splitStride = layout.gcnSplitStride; context.Function("vkCmdPushConstants")(commands, pipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(Push), &push); const auto groupsX = (columnEnd - window.columnBegin + 7u) / 8u; const auto groupsY = (rowEnd - window.rowBegin + 7u) / 8u; @@ -402,7 +414,7 @@ void TextureDetiler::DispatchImage(VkCommandBuffer commands, TextureTileMode til Require(ImageElementFormat(elementBytes) != VK_FORMAT_UNDEFINED, "unsupported element size for texture detiling through an image"); std::uint32_t columnEnd = 0, rowEnd = 0; checkWindow(layout, window, columnEnd, rowEnd); - const auto target = pipeline(tileMode, elementBytes, retile, false, true); + const auto target = pipeline(MipTileMode(tileMode, layout), elementBytes, retile, false, true); const auto alignment = std::max(context.limits.minStorageBufferOffsetAlignment, 4); const auto descriptorOffset = tiledOffset - tiledOffset % alignment; const auto base = tiledOffset - descriptorOffset; @@ -448,6 +460,7 @@ void TextureDetiler::DispatchImage(VkCommandBuffer commands, TextureTileMode til push.columnBegin = window.columnBegin; push.rowBegin = window.rowBegin; push.pipeBankXor = window.pipeBankXor; + push.splitStride = layout.gcnSplitStride; context.Function("vkCmdPushConstants")(commands, imagePipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(Push), &push); const auto groupsX = (columnEnd - window.columnBegin + 7u) / 8u; const auto groupsY = (rowEnd - window.rowBegin + 7u) / 8u; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp index 494c38359e..0d5bd91dca 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp @@ -59,6 +59,24 @@ BlockLayout GetBlockLayout(TextureTileMode tileMode, std::uint32_t bytesPerEleme case TextureTileMode::kS64KBX: case TextureTileMode::kD64KBX: case TextureTileMode::kR64KBX: return {65536u, 1u << kLog2BlockThin64KB[index].width, 1u << kLog2BlockThin64KB[index].height}; + case TextureTileMode::kGcnThin1D: + case TextureTileMode::kGcnDisplay1D: + case TextureTileMode::kGcnDisplay2D: + case TextureTileMode::kGcnThin2D: + case TextureTileMode::kGcnDepth2D64: + case TextureTileMode::kGcnDepth2D128: + case TextureTileMode::kGcnDepth2D256: + case TextureTileMode::kGcnDepth2D512: + case TextureTileMode::kGcnDepth2D1K: + case TextureTileMode::kGcnDepthPrt256: + case TextureTileMode::kGcnDepthPrt1K: + case TextureTileMode::kGcnDisplayPrt: + case TextureTileMode::kGcnDisplay2DPrt: + case TextureTileMode::kGcnThinPrt: + case TextureTileMode::kGcnThin2DPrt: { + const auto geometry = GcnTiling(tileMode, bytesPerElement); + return {geometry.blockBytes, geometry.blockWidth, geometry.blockHeight}; + } } throw std::runtime_error("AGC graphics: GetBlockLayout encountered an unknown tile mode"); } @@ -101,7 +119,22 @@ bool GetMipTailLayout(TextureTileMode tileMode, const BlockLayout& block, std::u switch (tileMode) { case TextureTileMode::kLinear: case TextureTileMode::kStandard256B: - case TextureTileMode::kD256B: return false; + case TextureTileMode::kD256B: + case TextureTileMode::kGcnThin1D: + case TextureTileMode::kGcnDisplay1D: + case TextureTileMode::kGcnDisplay2D: + case TextureTileMode::kGcnThin2D: + case TextureTileMode::kGcnDepth2D64: + case TextureTileMode::kGcnDepth2D128: + case TextureTileMode::kGcnDepth2D256: + case TextureTileMode::kGcnDepth2D512: + case TextureTileMode::kGcnDepth2D1K: + case TextureTileMode::kGcnDepthPrt256: + case TextureTileMode::kGcnDepthPrt1K: + case TextureTileMode::kGcnDisplayPrt: + case TextureTileMode::kGcnDisplay2DPrt: + case TextureTileMode::kGcnThinPrt: + case TextureTileMode::kGcnThin2DPrt: return false; case TextureTileMode::kStandard4KB: case TextureTileMode::kD4KB: case TextureTileMode::kS4KBX: @@ -248,6 +281,7 @@ std::vector ComputeMipLayout(TextureTileMode tileMode, std::uint3 Require(width != 0 && height != 0, "cannot compute mip layout for a zero-sized texture"); Require(mipCount != 0 && mipCount <= 16u, "texture mip count is out of range"); + Require(!IsGcnTiled(tileMode), "PS4 surfaces are laid out by ComputeGcnMipLayout from their pitch"); const auto bytesPerElement = BytesPerElement(format); const auto texelWidth = BlockWidth(format); const auto texelHeight = BlockHeight(format); @@ -279,7 +313,7 @@ std::array ThinBlockLayout(TextureTileMode tileMode, std::uint std::vector> CoveredMipBytes(TextureTileMode tileMode, std::uint32_t bytesPerElement, const TileMipLayout& mip) { std::vector> covered; - if (mip.tail || mip.width == 0 || mip.height == 0) return covered; + if (mip.tail || mip.width == 0 || mip.height == 0 || mip.gcnSplitStride != 0) return covered; const auto add = [&](std::uint64_t begin, std::uint64_t end) { end = std::min(end, mip.tiledSize); if (begin >= end) return; @@ -291,7 +325,7 @@ std::vector> CoveredMipBytes(TextureTile for (std::uint32_t row = 0; row < mip.height; ++row) add(static_cast(row) * mip.pitchBytes, static_cast(row) * mip.pitchBytes + rowBytes); return covered; } - const auto block = ThinBlockLayout(tileMode, bytesPerElement); + const auto block = ThinBlockLayout(MipTileMode(tileMode, mip), bytesPerElement); const auto wholeColumns = std::min(mip.width / block[1], mip.blocksPerRow); const auto wholeRows = mip.height / block[2]; for (std::uint32_t row = 0; row < wholeRows && wholeColumns != 0; ++row) { @@ -382,7 +416,170 @@ ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, s return result; } +std::vector ComputeGcnMipLayout(TextureTileMode tileMode, std::uint32_t format, std::uint32_t width, std::uint32_t height, std::uint32_t pitch, bool pow2Pad, std::uint32_t mipCount, bool linearGeneral, std::uint32_t layers) { + Require(layers != 0, "a PS4 surface has at least one slice"); + Require(!linearGeneral || tileMode == TextureTileMode::kLinear, "LINEAR_GENERAL is a linear layout"); + Require(width != 0 && height != 0 && pitch >= width, "cannot compute the mip layout of an empty PS4 texture"); + Require(mipCount != 0 && mipCount <= 16u, "texture mip count is out of range"); + Require(tileMode == TextureTileMode::kLinear || IsGcnTiled(tileMode), "PS4 texture tile mode has no modeled layout"); + const auto bytes = BytesPerElement(format); + const auto texelWidth = BlockWidth(format); + const auto texelHeight = BlockHeight(format); + std::vector mips(mipCount); + std::uint64_t offset = 0, linearOffset = 0; + for (std::uint32_t level = 0; level < mipCount; ++level) { + auto levelWidth = std::max(pitch >> level, 1u); + auto levelHeight = std::max(height >> level, 1u); + if (pow2Pad && level != 0) { + levelWidth = std::bit_ceil(levelWidth); + levelHeight = std::bit_ceil(levelHeight); + } + auto rowElements = (levelWidth + texelWidth - 1u) / texelWidth; + auto rows = (levelHeight + texelHeight - 1u) / texelHeight; + bool macro = false; + std::uint32_t macroWidth = 0; + std::uint32_t splitSlices = 1; + if (IsGcnMacroTiled(tileMode)) { + const auto geometry = GcnTiling(tileMode, bytes); + macro = level == 0 || (rowElements >= geometry.blockWidth && rows >= geometry.blockHeight); + if (macro) { + macroWidth = geometry.blockWidth; + splitSlices = geometry.splitSlices; + rowElements = AlignUp(rowElements, geometry.blockWidth); + rows = AlignUp(rows, geometry.blockHeight); + } + } + const bool micro = !macro && tileMode != TextureTileMode::kLinear; + if (micro) { + rowElements = AlignUp(rowElements, 8u); + rows = AlignUp(rows, 8u); + while (static_cast(rowElements) * rows * bytes % 256u != 0) rowElements += 8u; + } else if (!macro && !linearGeneral) { + const auto pitchAlign = std::max(8u, 64u / bytes); + const auto sliceAlign = std::max(64u, 256u / bytes); + rowElements = AlignUp(rowElements, pitchAlign); + while (static_cast(rowElements) * rows % sliceAlign != 0) rowElements += pitchAlign; + } + if (level == 0) Require(rowElements == (pitch + texelWidth - 1u) / texelWidth, "PS4 texture pitch " + std::to_string(pitch) + " is not padded the way its tile mode pads a row (" + std::to_string(rowElements * texelWidth) + ")"); + auto& mip = mips[level]; + mip.width = TexelLevelDimension(width, level, texelWidth); + mip.height = TexelLevelDimension(height, level, texelHeight); + mip.blocksPerRow = macro ? rowElements / macroWidth : micro ? rowElements / 8u : rowElements; + mip.gcnMicroTiled = micro && IsGcnMacroTiled(tileMode); + mip.gcnSplitStride = splitSlices > 1u ? static_cast(static_cast(rowElements) * rows * bytes / splitSlices) : 0u; + mip.pitchBytes = rowElements * bytes; + mip.tiledSize = static_cast(rowElements) * rows * bytes; + mip.linearSize = static_cast(mip.pitchBytes) * mip.height; + mip.tiledOffset = offset; + mip.linearOffset = linearOffset; + mip.tail = false; + mip.tailX = 0; + mip.tailY = 0; + offset += mip.tiledSize * layers; + linearOffset += mip.tiledSize; + } + return mips; +} + +GcnTileGeometry GcnTiling(TextureTileMode tileMode, std::uint32_t bytesPerElement) { + Require(IsGcnTiled(tileMode), "PS4 tile geometry needs a PS4 tiled mode"); + Require(std::has_single_bit(bytesPerElement) && bytesPerElement <= 16u, "unsupported bytes per element for PS4 tiling"); + const auto x = [](std::uint32_t bit) { return 1u << bit; }; + const auto y = [](std::uint32_t bit) { return 1u << (12u + bit); }; + const std::array thin{x(0), y(0), x(1), y(1), x(2), y(2)}; + const std::array, 5> display{{ + {x(0), x(1), x(2), y(1), y(0), y(2)}, {x(0), x(1), x(2), y(0), y(1), y(2)}, {x(0), x(1), y(0), x(2), y(1), y(2)}, + {x(0), y(0), x(1), x(2), y(1), y(2)}, {x(0), y(0), x(1), y(1), x(2), y(2)}, + }}; + const auto log2Bytes = static_cast(std::countr_zero(bytesPerElement)); + const bool displayOrder = tileMode == TextureTileMode::kGcnDisplay1D || tileMode == TextureTileMode::kGcnDisplay2D || tileMode == TextureTileMode::kGcnDisplayPrt || tileMode == TextureTileMode::kGcnDisplay2DPrt; + const auto& order = displayOrder ? display[log2Bytes] : thin; + const auto microBytes = 64u * bytesPerElement; + GcnTileGeometry geometry{}; + if (IsGcnMicroTiled(tileMode)) { + geometry = {microBytes, 8u, 8u, {}}; + for (std::uint32_t bit = 0; bit < order.size(); ++bit) geometry.equation[log2Bytes + bit] = order[bit]; + return geometry; + } + struct MacroMode { std::uint32_t bankHeight; std::uint32_t banks; std::uint32_t aspect; }; + constexpr std::array macroModes{{{4u, 16u, 4u}, {2u, 16u, 2u}, {1u, 16u, 2u}, {1u, 16u, 2u}, {1u, 8u, 1u}}}; + constexpr std::array prtMacroModes{{{8u, 16u, 4u}, {4u, 16u, 4u}, {2u, 16u, 2u}, {1u, 16u, 2u}, {1u, 8u, 1u}}}; + std::uint32_t tileSplit = std::min(1024u, std::max(256u, 2u * microBytes)); + switch (tileMode) { + case TextureTileMode::kGcnDepth2D64: tileSplit = 64u; break; + case TextureTileMode::kGcnDepth2D128: tileSplit = 128u; break; + case TextureTileMode::kGcnDepth2D256: + case TextureTileMode::kGcnDepthPrt256: tileSplit = 256u; break; + case TextureTileMode::kGcnDepth2D512: tileSplit = 512u; break; + case TextureTileMode::kGcnDepth2D1K: + case TextureTileMode::kGcnDepthPrt1K: tileSplit = 1024u; break; + default: break; + } + const bool prt = IsGcnPrtTiled(tileMode); + const bool splitRotation = tileMode != TextureTileMode::kGcnDisplayPrt && tileMode != TextureTileMode::kGcnThinPrt; + const bool pipe8x16 = tileMode == TextureTileMode::kGcnDisplayPrt || tileMode == TextureTileMode::kGcnThinPrt; + const auto tileBytes = std::min(tileSplit, microBytes); + const auto log2TileBytes = static_cast(std::countr_zero(tileBytes)); + const auto& mode = (prt ? prtMacroModes : macroModes)[log2TileBytes - 6u]; + constexpr std::uint32_t pipes = 8u, log2Pipes = 3u, pipeInterleaveBits = 8u; + const auto log2BankHeight = static_cast(std::countr_zero(mode.bankHeight)); + const auto bankBits = static_cast(std::countr_zero(mode.banks)); + geometry.blockWidth = 8u * pipes * mode.aspect; + geometry.blockHeight = 8u * mode.bankHeight * mode.banks / mode.aspect; + const auto macroBytes = tileBytes * mode.bankHeight; + Require(macroBytes % 256u == 0, "PS4 macro tile is not a whole number of pipe interleaves"); + geometry.blockBytes = macroBytes * pipes * mode.banks; + const auto elementBits = log2TileBytes - log2Bytes; + geometry.splitSlices = microBytes / tileBytes; + for (auto bit = elementBits; bit < order.size(); ++bit) geometry.splitMasks[bit - elementBits] = order[bit]; + std::array inner{}; + for (std::uint32_t bit = 0; bit < elementBits; ++bit) inner[log2Bytes + bit] = order[bit]; + auto innerBits = log2TileBytes; + for (std::uint32_t bit = 0; bit < log2BankHeight; ++bit) inner[innerBits++] = y(3u + bit); + const auto pipe = pipe8x16 ? std::array{x(4) | y(3) | x(5), x(3) | y(4), x(5) | y(5)} : std::array{x(3) | y(3) | x(4), x(4) | y(4), x(5) | y(5)}; + const auto log2Aspect = static_cast(std::countr_zero(mode.aspect)); + const auto tx = [&](std::uint32_t bit) { return prt && bit >= log2Aspect ? 0u : x(3u + log2Pipes + bit); }; + const auto ty = [&](std::uint32_t bit) { return prt && bit >= bankBits - log2Aspect ? 0u : y(3u + log2BankHeight + bit); }; + std::array bank{}; + switch (mode.banks) { + case 16u: bank = {tx(0) | ty(3), tx(1) | ty(2) | ty(3), tx(2) | ty(1), tx(3) | ty(0)}; break; + case 8u: bank = {tx(0) | ty(2), tx(1) | ty(1) | ty(2), tx(2) | ty(0), 0u}; break; + default: Require(false, "PS4 macro tile bank count is not modeled"); + } + for (std::uint32_t split = 0; splitRotation && split < 4u && geometry.splitMasks[split] != 0; ++split) { + const auto rotation = ((mode.banks / 2u + 1u) << split) & (mode.banks - 1u); + for (std::uint32_t bit = 0; bit < bankBits; ++bit) { + if ((rotation >> bit) & 1u) bank[bit] ^= geometry.splitMasks[split]; + } + } + Require(innerBits + log2Pipes + bankBits <= 16u, "PS4 macro tile exceeds 64 KiB"); + for (std::uint32_t bit = 0; bit < pipeInterleaveBits; ++bit) geometry.equation[bit] = inner[bit]; + for (std::uint32_t bit = 0; bit < log2Pipes; ++bit) geometry.equation[pipeInterleaveBits + bit] = pipe[bit]; + for (std::uint32_t bit = 0; bit < bankBits; ++bit) geometry.equation[pipeInterleaveBits + log2Pipes + bit] = bank[bit]; + for (auto bit = pipeInterleaveBits; bit < innerBits; ++bit) geometry.equation[bit + log2Pipes + bankBits] = inner[bit]; + return geometry; +} + +namespace { + +SurfaceGeometry describeGcnSurface(const GuestTextureResource& descriptor) { + Require(descriptor.dimension != TextureDimension::k3D && descriptor.dimension != TextureDimension::k2DMsaa, "PS4 3D and multisampled textures are not implemented"); + const bool layered = descriptor.dimension == TextureDimension::k2DArray || descriptor.dimension == TextureDimension::k1DArray || descriptor.dimension == TextureDimension::kCube; + SurfaceGeometry geometry; + geometry.layers = layered ? descriptor.depthOrLastArray + 1u : 1u; + geometry.levelMajor = geometry.layers > 1u && descriptor.mipCount > 1u; + geometry.mips = ComputeGcnMipLayout(descriptor.tileMode, descriptor.format, descriptor.width, descriptor.height, descriptor.gcnPitch, descriptor.gcnPow2Pad, descriptor.mipCount, descriptor.gcnLinearGeneral, geometry.layers); + geometry.imageLayers = geometry.layers; + geometry.guestBytes = geometry.mips.back().tiledOffset + geometry.mips.back().tiledSize * geometry.layers; + geometry.layerBytes = geometry.guestBytes / geometry.layers; + for (const auto& mip : geometry.mips) geometry.sliceLinearBytes = std::max(geometry.sliceLinearBytes, mip.linearOffset + mip.linearSize); + return geometry; +} + +} + SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor) { + if (descriptor.gcnPitch != 0) return describeGcnSurface(descriptor); SurfaceGeometry geometry; if (descriptor.samples != 1u && descriptor.tileMode != TextureTileMode::kLinear) { Require(descriptor.samples == 2u || descriptor.samples == 4u || descriptor.samples == 8u, "multisampled surface fragment count is not implemented"); @@ -510,6 +707,7 @@ GuestTextureResource ArrayLayerWindow(const GuestTextureResource& viewed, std::u std::vector ComputeElementMipLayout(TextureTileMode tileMode, std::uint32_t bytesPerElement, std::uint32_t width, std::uint32_t height, std::uint32_t mipCount) { Require(width != 0 && height != 0 && mipCount != 0 && mipCount <= 16u, "invalid surface mip chain"); + Require(!IsGcnTiled(tileMode), "PS4 surfaces are laid out by ComputeGcnMipLayout from their pitch"); if (tileMode == TextureTileMode::kLinear) return ComputeLinearMipLayout(bytesPerElement, 1u, 1u, width, height, mipCount); return ComputeTiledMipLayout(tileMode, bytesPerElement, 1u, 1u, width, height, mipCount); } diff --git a/core/libs/prx/libSceAgcDriver/tests/RegisterKey.cpp b/core/libs/prx/libSceAgcDriver/tests/RegisterKey.cpp index f9f295a3d3..430cad5fa4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/RegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/RegisterKey.cpp @@ -23,16 +23,16 @@ std::uint64_t baselineRegisterStateKey(const QueueState& queue, bool allUserWord key ^= value; key *= 0x100000001b3ull; }; - const auto userEnd = [&](std::uint32_t base) { - if (allUserWords) return base + 32u; + const auto userEnd = [&](std::uint32_t base, std::uint32_t limit) { + if (allUserWords) return base + limit; const auto resources = queue.shader.find(base - 1); if (resources == queue.shader.end()) return base; const auto count = ((resources->second >> 1u) & 0x1fu) | (((resources->second >> 27u) & 1u) << 5u); - return base + std::min(count, 32u); + return base + std::min(count, limit); }; - const std::array, 3> users{{{0x00cu, userEnd(0x00cu)}, {0x08cu, userEnd(0x08cu)}, {0x10cu, userEnd(0x10cu)}}}; + const std::array, 4> users{{{0x00cu, userEnd(0x00cu, 32u), 32u}, {0x04cu, userEnd(0x04cu, 16u), 16u}, {0x08cu, userEnd(0x08cu, 32u), 32u}, {0x10cu, userEnd(0x10cu, 32u), 32u}}}; const auto unread = [&](std::uint32_t offset) { - return std::any_of(users.begin(), users.end(), [&](const auto& user) { return offset >= user.second && offset < user.first + 32u; }); + return std::any_of(users.begin(), users.end(), [&](const auto& user) { return offset >= user[1] && offset < user[0] + user[2]; }); }; for (const auto& range : Graphics::DrawKeyRegisters) { const auto& bank = range.bank == Graphics::RegisterBank::Context ? queue.context : range.bank == Graphics::RegisterBank::Shader ? queue.shader : queue.userConfig; diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp index 5874fbb5c2..c773ddb446 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp @@ -30,6 +30,7 @@ struct Push { std::uint32_t columnBegin; std::uint32_t rowBegin; std::uint32_t pipeBankXor; + std::uint32_t splitStride; }; Push decodePush(const std::vector& bytes) { diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp index fce3aa0322..ddd5a4b118 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp @@ -140,9 +140,144 @@ void requireSliceAddresses(const GuestTextureResource& resource, std::uint32_t b } } +std::uint64_t referenceGcnAddress(TextureTileMode mode, std::uint32_t bytes, std::uint32_t pitch, std::uint32_t height, std::uint32_t x, std::uint32_t y) { + const auto bit = [](std::uint32_t value, std::uint32_t index) { return (value >> index) & 1u; }; + const std::uint32_t x0 = bit(x, 0), x1 = bit(x, 1), x2 = bit(x, 2), y0 = bit(y, 0), y1 = bit(y, 1), y2 = bit(y, 2); + std::array pixel{x0, y0, x1, y1, x2, y2}; + if (mode == TextureTileMode::kGcnDisplay1D || mode == TextureTileMode::kGcnDisplay2D || mode == TextureTileMode::kGcnDisplayPrt || mode == TextureTileMode::kGcnDisplay2DPrt) { + switch (bytes) { + case 1u: pixel = {x0, x1, x2, y1, y0, y2}; break; + case 2u: pixel = {x0, x1, x2, y0, y1, y2}; break; + case 4u: pixel = {x0, x1, y0, x2, y1, y2}; break; + case 8u: pixel = {x0, y0, x1, x2, y1, y2}; break; + default: pixel = {x0, y0, x1, y1, x2, y2}; break; + } + } + std::uint32_t pixelIndex = 0; + for (std::uint32_t index = 0; index < 6u; ++index) pixelIndex |= pixel[index] << index; + std::uint64_t microBytes = 64u * bytes; + std::uint64_t elementOffset = static_cast(pixelIndex) * bytes; + if (mode == TextureTileMode::kGcnThin1D || mode == TextureTileMode::kGcnDisplay1D) { + return (static_cast(y / 8u) * (pitch / 8u) + x / 8u) * microBytes + elementOffset; + } + std::uint32_t tileSplit = std::min(1024u, std::max(256u, 2u * static_cast(microBytes))); + if (mode >= TextureTileMode::kGcnDepth2D64 && mode <= TextureTileMode::kGcnDepth2D1K) tileSplit = 64u << (static_cast(mode) - static_cast(TextureTileMode::kGcnDepth2D64)); + if (mode == TextureTileMode::kGcnDepthPrt256) tileSplit = 256u; + if (mode == TextureTileMode::kGcnDepthPrt1K) tileSplit = 1024u; + const bool prt = IsGcnPrtTiled(mode); + const auto macroMode = std::bit_width(std::min(tileSplit, microBytes) / 64u) - 1u + (prt ? 8u : 0u); + std::uint64_t splitSlice = 0; + if (microBytes > tileSplit) { + splitSlice = elementOffset / tileSplit; + elementOffset %= tileSplit; + microBytes = tileSplit; + } + constexpr std::uint32_t bankHeights[] = {4u, 2u, 1u, 1u, 1u, 0u, 0u, 0u, 8u, 4u, 2u, 1u, 1u}, bankCounts[] = {16u, 16u, 16u, 16u, 8u, 0u, 0u, 0u, 16u, 16u, 16u, 16u, 8u}, aspects[] = {4u, 2u, 2u, 2u, 1u, 0u, 0u, 0u, 4u, 4u, 2u, 2u, 1u}; + const std::uint32_t pipes = 8u, bankWidth = 1u, bankHeight = bankHeights[macroMode], banks = bankCounts[macroMode], aspect = aspects[macroMode]; + const std::uint32_t macroPitch = 8u * bankWidth * pipes * aspect; + const std::uint32_t macroHeight = 8u * bankHeight * banks / aspect; + const std::uint64_t macroBytes = microBytes * (macroPitch / 8u) * (macroHeight / 8u) / (pipes * banks); + const std::uint64_t macroOffset = (static_cast(y / macroHeight) * (pitch / macroPitch) + x / macroPitch) * macroBytes; + const std::uint32_t tileIndex = ((y / 8u) % bankHeight) * bankWidth + ((x / 8u) / pipes) % bankWidth; + const std::uint64_t sliceBytes = static_cast(pitch / macroPitch) * (height / macroHeight) * macroBytes; + const std::uint64_t total = splitSlice * sliceBytes + macroOffset + elementOffset + static_cast(tileIndex) * microBytes; + const std::uint32_t px = prt ? x % macroPitch : x, py = prt ? y % macroHeight : y; + const std::uint32_t tx = px / 8u, ty = py / 8u; + const bool pipe8x16 = mode == TextureTileMode::kGcnDisplayPrt || mode == TextureTileMode::kGcnThinPrt; + const std::uint32_t pipe = pipe8x16 ? (bit(tx, 1) ^ bit(ty, 0) ^ bit(tx, 2)) | ((bit(tx, 0) ^ bit(ty, 1)) << 1u) | ((bit(tx, 2) ^ bit(ty, 2)) << 2u) + : (bit(tx, 0) ^ bit(ty, 0) ^ bit(tx, 1)) | ((bit(tx, 1) ^ bit(ty, 1)) << 1u) | ((bit(tx, 2) ^ bit(ty, 2)) << 2u); + const std::uint32_t bx = px / 8u / (bankWidth * pipes), by = py / 8u / bankHeight; + std::uint32_t bank = 0; + if (banks == 16u) bank = (bit(bx, 0) ^ bit(by, 3)) | ((bit(bx, 1) ^ bit(by, 2) ^ bit(by, 3)) << 1u) | ((bit(bx, 2) ^ bit(by, 1)) << 2u) | ((bit(bx, 3) ^ bit(by, 0)) << 3u); + else bank = (bit(bx, 0) ^ bit(by, 2)) | ((bit(bx, 1) ^ bit(by, 1) ^ bit(by, 2)) << 1u) | ((bit(bx, 2) ^ bit(by, 0)) << 2u); + if (!pipe8x16) bank = (bank ^ static_cast((banks / 2u + 1u) * splitSlice)) & (banks - 1u); + const std::uint32_t bankBits = static_cast(std::countr_zero(banks)); + return ((total >> 8u) << (8u + 3u + bankBits)) | (static_cast(bank) << 11u) | (static_cast(pipe) << 8u) | (total & 0xffu); +} + +void gcnTilingTests() { + using enum TextureTileMode; + for (const auto mode : {kGcnThin1D, kGcnDisplay1D, kGcnDisplay2D, kGcnThin2D, kGcnDepth2D64, kGcnDepth2D128, kGcnDepth2D256, kGcnDepth2D512, kGcnDepth2D1K, kGcnDepthPrt256, kGcnDepthPrt1K, kGcnDisplayPrt, kGcnDisplay2DPrt, kGcnThinPrt, kGcnThin2DPrt}) { + for (std::uint32_t bytes = 1u; bytes <= 16u; bytes <<= 1u) { + const auto geometry = GcnTiling(mode, bytes); + const auto what = "PS4 tile mode " + std::to_string(static_cast(mode)) + " at " + std::to_string(bytes) + " bytes"; + Require(geometry.blockBytes * geometry.splitSlices == geometry.blockWidth * geometry.blockHeight * bytes, what + ": a block holds its elements exactly"); + const auto splitStride = 16u * geometry.blockBytes; + const auto pitch = geometry.blockWidth * 4u; + const auto height = geometry.blockHeight * 4u; + std::vector seen(static_cast(pitch) * height, 0u); + for (std::uint32_t y = 0; y < height; ++y) { + for (std::uint32_t x = 0; x < pitch; ++x) { + std::uint32_t inBlock = 0; + for (std::uint32_t bit = 0; bit < 16u; ++bit) inBlock |= static_cast(std::popcount((x & geometry.equation[bit] & 0xfffu) ^ ((y << 12u) & geometry.equation[bit] & 0xfff000u)) & 1) << bit; + const auto block = static_cast(y / geometry.blockHeight) * (pitch / geometry.blockWidth) + x / geometry.blockWidth; + std::uint32_t split = 0; + for (std::uint32_t bit = 0; bit < 4u; ++bit) split |= static_cast(std::popcount((x & geometry.splitMasks[bit] & 0xfffu) ^ ((y << 12u) & geometry.splitMasks[bit] & 0xfff000u)) & 1) << bit; + const auto address = block * geometry.blockBytes + inBlock + static_cast(split) * splitStride; + if (address != referenceGcnAddress(mode, bytes, pitch, height, x, y)) Require(false, what + ": element " + std::to_string(x) + "," + std::to_string(y) + " is at " + std::to_string(address) + ", addrlib places it at " + std::to_string(referenceGcnAddress(mode, bytes, pitch, height, x, y))); + Require(address % bytes == 0 && address / bytes < seen.size() && seen[address / bytes]++ == 0, what + ": two elements share an address or one leaves the surface"); + } + } + } + } + constexpr std::array, 5> display2DPrt{{{256u, 256u}, {256u, 128u}, {128u, 128u}, {128u, 64u}, {64u, 64u}}}; + for (std::uint32_t log2 = 0; log2 < 5u; ++log2) { + const auto geometry = GcnTiling(TextureTileMode::kGcnDisplay2DPrt, 1u << log2); + Require(geometry.blockWidth == display2DPrt[log2].first && geometry.blockHeight == display2DPrt[log2].second, "PS4 Display2DThinPrt macro tile extent changed"); + } + { + Require(ComputeGcnMipLayout(TextureTileMode::kLinear, 20u, 100u, 3u, 100u, false, 1u, true)[0].pitchBytes == 400u, "PS4 linear general pitch changed"); + reject([] { ComputeGcnMipLayout(TextureTileMode::kLinear, 20u, 100u, 3u, 100u, false, 1u); }, "is not padded the way its tile mode pads a row"); + } + constexpr std::array, 5> display2D{{{256u, 128u}, {128u, 128u}, {128u, 64u}, {128u, 64u}, {64u, 64u}}}; + for (std::uint32_t log2 = 0; log2 < 5u; ++log2) { + const auto geometry = GcnTiling(TextureTileMode::kGcnDisplay2D, 1u << log2); + Require(geometry.blockWidth == display2D[log2].first && geometry.blockHeight == display2D[log2].second, "PS4 Display2DThin macro tile extent changed"); + } + { + GuestTextureResource array{}; + array.baseAddress = 0x10000; + array.width = 16u; + array.height = 16u; + array.depthOrLastArray = 2u; + array.mipCount = 3u; + array.lastLevel = 2u; + array.tileMode = TextureTileMode::kGcnThin1D; + array.dimension = TextureDimension::k2DArray; + array.format = 20u; + array.gcnPitch = 16u; + const auto geometry = DescribeSurface(array); + Require(geometry.levelMajor && geometry.layers == 3u && geometry.guestBytes == 4608u, "PS4 layered mip chain size changed"); + Require(geometry.mips[1].tiledOffset == 3072u && geometry.mips[2].tiledOffset == 3840u && geometry.mips[1].tiledSize == 256u, "PS4 levels do not follow every slice of the level before"); + Require(geometry.GuestOffset(2u, geometry.mips[1]) == 3584u && geometry.GuestOffset(1u, geometry.mips[0]) == 1024u, "PS4 slice of a level is misplaced"); + Require(geometry.mips[1].linearOffset == 1024u && geometry.mips[2].linearOffset == 1280u, "PS4 linear staging is not per slice"); + array.mipCount = 1u; + array.lastLevel = 0u; + Require(!DescribeSurface(array).levelMajor, "a single-level PS4 array is slice-major"); + } + { + const auto mips = ComputeGcnMipLayout(TextureTileMode::kGcnDepth2D64, 20u, 2048u, 1152u, 2048u, false, 1u); + Require(mips[0].tiledSize == 2048u * 1152u * 4u && mips[0].gcnSplitStride == 2048u * 1152u, "PS4 tile-split depth layout changed"); + Require(GcnTiling(TextureTileMode::kGcnDepth2D64, 4u).splitSlices == 4u && GcnTiling(TextureTileMode::kGcnDepth2D256, 4u).splitSlices == 1u, "PS4 depth split slice count changed"); + } + { + const auto mips = ComputeGcnMipLayout(TextureTileMode::kGcnThin1D, 14u, 212u, 35u, 224u, false, 1u); + Require(mips[0].tiledSize == 224u * 40u * 2u && mips[0].blocksPerRow == 28u && mips[0].pitchBytes == 448u && !mips[0].gcnMicroTiled, "PS4 1D thin texture layout changed"); + reject([] { ComputeGcnMipLayout(TextureTileMode::kGcnThin1D, 14u, 212u, 35u, 216u, false, 1u); }, "is not padded the way its tile mode pads a row"); + } + { + const auto mips = ComputeGcnMipLayout(TextureTileMode::kGcnDisplay2D, 56u, 256u, 256u, 256u, false, 4u); + Require(!mips[0].gcnMicroTiled && !mips[1].gcnMicroTiled && mips[2].gcnMicroTiled && mips[3].gcnMicroTiled, "PS4 2D tiled mips switch to 1D tiling below one macro tile"); + Require(mips[0].tiledOffset == 0u && mips[1].tiledOffset == 256u * 256u * 4u && mips[1].blocksPerRow == 1u, "PS4 mips follow level 0 in order"); + Require(mips[2].tiledOffset == mips[1].tiledOffset + 128u * 128u * 4u && mips[2].blocksPerRow == 8u, "PS4 1D tiled mip after the 2D levels"); + Require(MipTileMode(TextureTileMode::kGcnDisplay2D, mips[2]) == TextureTileMode::kGcnDisplay1D, "a 1D tiled level of a 2D surface uses the 1D display mode"); + } +} + } void RunTextureTilingTests() { + gcnTilingTests(); { const auto mips = ComputeMipLayout(TextureTileMode::kLinear, 1, 4, 4, 2); Require(mips.size() == 2, "linear mip chain must contain the requested mip count"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GcnDetile.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GcnDetile.cpp new file mode 100644 index 0000000000..43a63b3fe3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GcnDetile.cpp @@ -0,0 +1,91 @@ +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using namespace AgcDriver::Graphics; + +std::uint32_t parity(std::uint32_t mask, std::uint32_t x, std::uint32_t y) { + return static_cast(std::popcount((x & mask & 0xfffu) ^ ((y << 12u) & mask & 0xfff000u))) & 1u; +} + +std::uint8_t elementByte(std::uint32_t x, std::uint32_t y, std::uint32_t byte) { + return static_cast(x * 7u + y * 13u + byte * 101u + (x >> 4u) * 3u + (y >> 3u) * 5u); +} + +void detile(const Context& context, TextureDetiler& detiler, TextureTileMode mode, std::uint32_t bytes) { + constexpr std::array formats{1u, 7u, 20u, 62u, 75u}; + const auto geometry = GcnTiling(mode, bytes); + const auto width = geometry.blockWidth * 2u; + const auto height = geometry.blockHeight * 2u; + const auto mip = ComputeGcnMipLayout(mode, formats[std::countr_zero(bytes)], width, height, width, false, 1u).front(); + const auto what = "PS4 tile mode " + std::to_string(static_cast(mode)) + " at " + std::to_string(bytes) + " bytes"; + Buffer tiled(context, static_cast(mip.tiledSize), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer linear(context, static_cast(mip.linearSize), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + std::memset(tiled.Bytes().data(), 0, tiled.Bytes().size()); + std::memset(linear.Bytes().data(), 0xcd, linear.Bytes().size()); + for (std::uint32_t y = 0; y < height; ++y) { + for (std::uint32_t x = 0; x < width; ++x) { + std::uint32_t inBlock = 0, split = 0; + for (std::uint32_t bit = 0; bit < 16u; ++bit) inBlock |= parity(geometry.equation[bit], x, y) << bit; + for (std::uint32_t bit = 0; bit < 4u; ++bit) split |= parity(geometry.splitMasks[bit], x, y) << bit; + const auto block = static_cast(y / geometry.blockHeight) * mip.blocksPerRow + x / geometry.blockWidth; + const auto offset = block * geometry.blockBytes + inBlock + static_cast(split) * mip.gcnSplitStride; + Require(offset + bytes <= tiled.Bytes().size(), what + ": an element lies outside the tiled surface"); + for (std::uint32_t byte = 0; byte < bytes; ++byte) tiled.Bytes()[offset + byte] = static_cast(elementByte(x, y, byte)); + } + } + CommandBatch batch(context); + detiler.BeginBatch(); + detiler.Dispatch(batch.Handle(), mode, bytes, tiled.Handle(), 0, linear.Handle(), 0, mip); + const VkMemoryBarrier toHost{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT}; + context.Function("vkCmdPipelineBarrier")(batch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &toHost, 0, nullptr, 0, nullptr); + batch.SubmitAndWait(); + for (std::uint32_t y = 0; y < height; ++y) { + for (std::uint32_t x = 0; x < width; ++x) { + for (std::uint32_t byte = 0; byte < bytes; ++byte) { + const auto got = static_cast(linear.Bytes()[static_cast(y) * mip.pitchBytes + static_cast(x) * bytes + byte]); + if (got != elementByte(x, y, byte)) Require(false, what + ": element " + std::to_string(x) + "," + std::to_string(y) + " byte " + std::to_string(byte) + " detiled as " + std::to_string(got) + ", expected " + std::to_string(elementByte(x, y, byte))); + } + } + } +} + +} + +int main() { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + TextureDetiler detiler(context); + using enum TextureTileMode; + for (const auto mode : {kGcnThin1D, kGcnDisplay1D, kGcnDisplay2D, kGcnThin2D, kGcnDepth2D64, kGcnDepth2D256, kGcnDepth2D1K, kGcnDisplayPrt, kGcnThin2DPrt, kGcnDepthPrt256}) { + for (std::uint32_t bytes = 1u; bytes <= 16u; bytes <<= 1u) detile(context, detiler, mode, bytes); + } + } + device.reset(); + std::puts("PS4 detile tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PatternBufferFill.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PatternBufferFill.cpp index f56fbe9c0a..7cf8e861e9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PatternBufferFill.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PatternBufferFill.cpp @@ -82,6 +82,29 @@ void RangeTests() { Require(!DecodePatternFillRange(data, packet), "multidimensional fill was accepted"); } +void GcnRangeTests() { + const ShaderRecompiler::ShaderComputeStageInfo compute{{64, 1, 1}, 0, {true, false, false}, false, 1}; + constexpr std::array data{0, 0, 0, 0, 0x80600830u, 0x00040002u, 1, 0x20024004u, 0x40a00000u, 0x00047002u, 0x001fe000u, 0x18024004u, 0x806008acu, 0x00100002u, 1, 0x20077facu}; + const auto& code = GcnPatternFillKernel; + Require(MatchesGcnPatternFillKernel(code, data, compute), "PS4 pattern fill kernel was not recognized"); + Require(!MatchesPatternFillKernel(code, data, compute), "the PS4 fill matched the PS5 kernel"); + for (std::size_t word = 0; word < code.size(); ++word) { + auto altered = code; + altered[word] ^= 1; + Require(!MatchesGcnPatternFillKernel(altered, data, compute), "a different shader matched the PS4 fill kernel"); + } + for (const auto& [word, bits] : std::array, 4>{{{5, 0x000c0000u}, {11, 0x00003000u}, {7, 0x00800000u}, {7, 0x00000001u}}}) { + auto altered = data; + altered[word] ^= bits; + Require(!MatchesGcnPatternFillKernel(code, altered, compute), "unsupported PS4 fill descriptor matched"); + } + constexpr std::array packet{0xc0031500u, 0x7f80u, 1, 1, 1}; + const auto range = DecodeGcnPatternFillRange(data, packet); + Require(range && range->source == 0x280600830ull && range->destination == 0x240a00000ull && range->control == 0x2806008acull, "PS4 fill addresses kept bits above 44"); + Require(range->invocations == 0x7f80u * 64u && range->records == 0x1fe000u, "PS4 fill range changed"); + Require(range->UniformBytes(0x1fe000u, 0) == 8355840u, "PS4 render target clear does not cover the target"); +} + std::vector Commands(std::span data, std::uint32_t threads, std::uint32_t wave, bool partial, bool indirect) { std::vector words; const auto registers = [&](std::uint32_t first, std::span values) { @@ -209,6 +232,7 @@ void Benchmark() { int main(int argc, char** argv) { try { RangeTests(); + GcnRangeTests(); { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; diff --git a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp index 31d7e6b4ae..bd7161adee 100644 --- a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp @@ -195,6 +195,10 @@ int main(int argc, char** argv) { } int failures = 0; for (int i = 1; i < argc; ++i) { + if (std::string(argv[i]) == "--gcn") { + ShaderRecompiler::SetGcnGuestShaders(true); + continue; + } if (std::string(argv[i]) == "--dis") { g_disassemble = true; continue; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 8de8cae97c..0226587fc6 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -76,6 +76,10 @@ - [HTILE address of a depth array slice](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `decodeDepth` adds the `DB_DEPTH_VIEW` slice offset to the Z and stencil addresses but not to `htileAddress`, so every slice of a layered depth target shares slice 0's HTILE address and a fill of slice 0's HTILE marks all slices cleared. Astro's Playroom fills the HTILE of each of its three shadow cascades 0x40000 apart (0x24000 bytes of tiles each) back to back before drawing, so the result is still right there; a title that clears one slice between draws to another would lose depth. The per-slice HTILE stride is not derived yet. - [sceAgcDriverGetHsOffchipParam](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver, NID `r28hEh6cNH0`) - declared as a throwing stub. The two `uint16_t` output pointers and integer return type follow [ps5-vulkan](https://github.com/blackbearreloaded/ps5-vulkan/blob/4bb0c8b1423698a7369efc22b829bf16a99e7128/native/tess_driver_import_stub.c#L10), which reads the pair before it binds a tessellation factor ring. The meaning of the two values, their initial state and how they relate to `sceAgcDriverSetHsOffchipParam`, which keeps nothing here, are unknown; the stub does not write caller memory. PPSA99011 imports it - [Vertex input and color target overlap](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (AGC driver) - direct draws exclude the descriptor prefix skipped by `firstVertex` or `firstInstance` from the alias check; indexed draws conservatively use index zero as the lower bound before adding `firstVertex`. Indirect draws still check the full descriptor extent. Reads of the active target's bytes remain unsupported; same-draw console ordering is not established. +- [PS4 (GCN) tiling](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) (libSceAgcDriver, PS4) - not checked on a console. Surfaces with `GuestTextureResource::gcnPitch` set use the GB_TILE_MODE and GB_MACROTILE_MODE tables of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/video_core/amdgpu/tiling.cpp) and addrlib's `ComputeSurfaceAddrFromCoordMicroTiled` and `MacroTiled`: 8x8 micro tiles in thin, depth or display element order, macro tiles with pipe config P8_32x32_16x16 (P8_32x32_8x16 for the two PRT thin modes), bank width 1 and a 256-byte pipe interleave, depth micro tiles split into slices a whole surface apart with the bank rotated by (banks / 2 + 1) per slice, PRT pipe and bank taken from the coordinates inside the macro tile, and LINEAR_GENERAL rows of exactly the pitch. [TextureTiling.cpp](../../../core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp) compares it with a separate implementation of the addrlib formulas. +- [PS4 (GCN) thick and layered surfaces](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) (libSceAgcDriver, PS4) - 3D and thick modes (GB_TILE_MODE 15 and 19 to 26) throw, and so does a storage image or a depth plane of a layered surface with several mip levels. A sampled layered surface with several mip levels is stored level by level, which is not checked on a console; [GcnDetile.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/GcnDetile.cpp) runs the GPU detiler over every mode that does not throw. +- [PS4 render targets and display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp) (libSceAgcDriver, PS4) - a tiled PS4 display buffer is read as Display2DThin with rows padded to 128 elements, as shadPS4's `image_info.cpp` does. A PS4 render target spans its surface padded to whole macro tiles (1920x1080 at 32 bits is 1920x1088), the display buffer is its top-left corner, and a texture over that corner with the same pitch, tiling and texel size is a [GPU copy](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) of it. A PS4 `DB_Z_INFO` names a GB_TILE_MODE index; the depth image stays on the GPU and HTILE is kept, as for the PS5. +- [PS4 pattern fill](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) (libSceAgcDriver, PS4) - the PS4 build of the pattern-fill compute kernel seen in Limbo (CUSA01664), with user data s[4:7] a 32-bit UINT source, s[8:11] the 32-bit UINT destination and s[12:15] a control record of count and index mask, where thread i stores source[i & mask], runs as a GPU fill when the mask is 0, a single repeated dword; other masks run the kernel, since running Limbo's two-dword clears of a 64-bit target as fills made its gameplay advance more slowly per frame. The kernel is recognized by its code, not by an SDK symbol. ### Functional From d64ae425c2c7d24a9a3d1100fe14d4c9008d20ca Mon Sep 17 00:00:00 2001 From: Get-fix <100056769+Get-fix@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:49:25 +0300 Subject: [PATCH 079/160] test(libSceVideoOut): expect the PS4 alignment message in the PS4 flip test The 4 KiB display buffer alignment check now names linear and PS4 buffers, because PS4 tiled display buffers may be 4 KiB aligned too. video_out_flip_ps4 still looked for the old text when a PS4 buffer set is changed to the PS5 tiled layout, so it failed although the change is still refused. --- core/libs/prx/libSceVideoOut/tests/Flip.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index ca0ae05f8a..f4bfd75ccf 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -150,7 +150,7 @@ void testPs4Buffers() { tiledChange.width = 64; tiledChange.height = 64; tiledChange.pixel_format = 0x8000000000000000ull; - check(expectFailure([&] { sceVideoOutSubmitChangeBufferAttribute2(handle, 0, &tiledChange, nullptr); }).find("only supported for linear buffers") != std::string::npos, "a PS4 buffer set was changed to tiled"); + check(expectFailure([&] { sceVideoOutSubmitChangeBufferAttribute2(handle, 0, &tiledChange, nullptr); }).find("only supported for linear and PS4 buffers") != std::string::npos, "a PS4 buffer set was changed to tiled"); void* const odd[1] = {storage.data() + 65536 + 256}; check(expectFailure([&] { sceVideoOutRegisterBuffers(handle, 3, odd, 1, &attribute); }).find("4 KiB alignment") != std::string::npos, "a PS4 buffer was accepted below 4 KiB alignment"); void* const fourth[1] = {storage.data() + 65536}; From eefa50841267cd0bd5352af17248139445fb0613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= Date: Wed, 28 Oct 2026 13:01:55 +0100 Subject: [PATCH 080/160] fix(libSceAgcDriver): update the pipeline journal layout guards for ShaderStages::legacyVertex Co-Authored-By: Claude Sonnet 5.5 --- .../libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index 2b088647e5..7db1f03dca 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -38,8 +38,8 @@ static_assert(sizeof(VkStencilOpState) == 28, "VkStencilOpState changed: update static_assert(sizeof(VkVertexInputBindingDescription) == 12, "VkVertexInputBindingDescription changed: update the recipe codec"); static_assert(sizeof(VkVertexInputAttributeDescription) == 16, "VkVertexInputAttributeDescription changed: update the recipe codec"); #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) -static_assert(sizeof(State) == 912, "State changed: update CanonicalPipelineState and the recipe codec"); -static_assert(sizeof(ShaderStages) == 160, "ShaderStages changed: update the recipe codec"); +static_assert(sizeof(State) == 920, "State changed: update CanonicalPipelineState and the recipe codec"); +static_assert(sizeof(ShaderStages) == 164, "ShaderStages changed: update the recipe codec"); static_assert(sizeof(ShaderRecompiler::MeshConfiguration) == 40, "MeshConfiguration changed: update the recipe codec"); static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "TessellationConfiguration changed: update the recipe codec"); #endif From e8d2532535121a38a7e110543d29842a0d28e562 Mon Sep 17 00:00:00 2001 From: Kyon Date: Wed, 28 Oct 2026 12:23:24 +0000 Subject: [PATCH 081/160] feat(libSceBgft): declare 12 additional callable exports --- core/libs/prx/libSceBgft/Export.cpp | 59 +++++++++++++++++++ .../libSceBgft/include/InventoryExports.hpp | 20 +++++++ core/libs/prx/libSceBgft/libSceBgft.hpp | 1 + docs/dev/debt/libSceBgft.md | 19 ++++++ 4 files changed, 99 insertions(+) create mode 100644 core/libs/prx/libSceBgft/include/InventoryExports.hpp diff --git a/core/libs/prx/libSceBgft/Export.cpp b/core/libs/prx/libSceBgft/Export.cpp index c94cfeb6b8..422771488d 100644 --- a/core/libs/prx/libSceBgft/Export.cpp +++ b/core/libs/prx/libSceBgft/Export.cpp @@ -1,3 +1,5 @@ +#include "prx/libc/include/general/ExportMacros.hpp" +#include "prx/libSceBgft/include/InventoryExports.hpp" #include "prx/libSceBgft/include/Api.hpp" #include #include @@ -419,3 +421,60 @@ int APS5_VABI sceBgftServiceIntUploadSetUploadError() { } } + +extern "C" { + +int APS5_VABI sceBgftServiceIntDownloadClearAllInstalled() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceBgftServiceIntDownloadClearInstalled() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceBgftServiceIntDownloadPauseGroupTasks() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceBgftServiceIntDownloadResumeGroupTasks() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceBgftServiceIntDownloadStartGroupTasks() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceBgftServiceIntDownloadUnregisterCompleteTask() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceBgftServiceIntUploadUnregisterCompleteTask() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI libSceBgftUnknown_3cvL6QTobNg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("3cvL6QTobNg", libSceBgftUnknown_3cvL6QTobNg_nid_no_patch); + +int APS5_VABI libSceBgftUnknown_4JQKNtsVDpg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4JQKNtsVDpg", libSceBgftUnknown_4JQKNtsVDpg_nid_no_patch); + +int APS5_VABI libSceBgftUnknown_85axuUkg9eE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("85axuUkg9eE", libSceBgftUnknown_85axuUkg9eE_nid_no_patch); + +int APS5_VABI libSceBgftUnknown_fJnnIc5sHOU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("fJnnIc5sHOU", libSceBgftUnknown_fJnnIc5sHOU_nid_no_patch); + +int APS5_VABI libSceBgftUnknown_tBOVSvi9EjY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("tBOVSvi9EjY", libSceBgftUnknown_tBOVSvi9EjY_nid_no_patch); + +} diff --git a/core/libs/prx/libSceBgft/include/InventoryExports.hpp b/core/libs/prx/libSceBgft/include/InventoryExports.hpp new file mode 100644 index 0000000000..58677f4b4c --- /dev/null +++ b/core/libs/prx/libSceBgft/include/InventoryExports.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceBgftServiceIntDownloadClearAllInstalled(); +int APS5_VABI sceBgftServiceIntDownloadClearInstalled(); +int APS5_VABI sceBgftServiceIntDownloadPauseGroupTasks(); +int APS5_VABI sceBgftServiceIntDownloadResumeGroupTasks(); +int APS5_VABI sceBgftServiceIntDownloadStartGroupTasks(); +int APS5_VABI sceBgftServiceIntDownloadUnregisterCompleteTask(); +int APS5_VABI sceBgftServiceIntUploadUnregisterCompleteTask(); +int APS5_VABI libSceBgftUnknown_3cvL6QTobNg_nid_no_patch(); +int APS5_VABI libSceBgftUnknown_4JQKNtsVDpg_nid_no_patch(); +int APS5_VABI libSceBgftUnknown_85axuUkg9eE_nid_no_patch(); +int APS5_VABI libSceBgftUnknown_fJnnIc5sHOU_nid_no_patch(); +int APS5_VABI libSceBgftUnknown_tBOVSvi9EjY_nid_no_patch(); + +} diff --git a/core/libs/prx/libSceBgft/libSceBgft.hpp b/core/libs/prx/libSceBgft/libSceBgft.hpp index 27e9095851..637dd6d2f7 100644 --- a/core/libs/prx/libSceBgft/libSceBgft.hpp +++ b/core/libs/prx/libSceBgft/libSceBgft.hpp @@ -1,3 +1,4 @@ #pragma once #include "prx/libSceBgft/include/Api.hpp" +#include "prx/libSceBgft/include/InventoryExports.hpp" diff --git a/docs/dev/debt/libSceBgft.md b/docs/dev/debt/libSceBgft.md index 95b8d7ac06..2c75eea670 100644 --- a/docs/dev/debt/libSceBgft.md +++ b/docs/dev/debt/libSceBgft.md @@ -15,3 +15,22 @@ - [sceBgftServiceIntPlayGoCheckDiscIniChunksInstall and sceBgftServiceIntPlayGoStartDataDiscInstall](../../../core/libs/prx/libSceBgft/Export.cpp) (libSceBgft) - The list is the intersection of the export table of the console's libSceBgft (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceBgft.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceBgftServiceIntSetTaskVisible, sceBgftServiceIntUploadGetAvailableTaskCount, sceBgftServiceIntUploadGetTaskInfo and sceBgftServiceIntUploadHideTask](../../../core/libs/prx/libSceBgft/Export.cpp) (libSceBgft) - The list is the intersection of the export table of the console's libSceBgft (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceBgft.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceBgftServiceIntUploadSetStartState and sceBgftServiceIntUploadSetUploadError](../../../core/libs/prx/libSceBgft/Export.cpp) (libSceBgft) - The list is the intersection of the export table of the console's libSceBgft (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceBgft.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. + +### Additional callable export inventory + +- [Additional callable exports](../../../core/libs/prx/libSceBgft/Export.cpp) - 12 entries have import-resolution declarations only. Parameter lists, return types and behavior are unverified; parameterless `int` declarations are placeholders, not verified ABI contracts. Every entry throws through `NotImplemented_nid_no_patch`. Unidentified names use literal NID aliases. No runtime support, implemented-function increase or title unblock is claimed. + +| Name | NID | +| --- | --- | +| `sceBgftServiceIntDownloadClearAllInstalled` | `XEJKc+Ivvm4` | +| `sceBgftServiceIntDownloadClearInstalled` | `bmSUGdmdJto` | +| `sceBgftServiceIntDownloadPauseGroupTasks` | `cHMDnBGpQoo` | +| `sceBgftServiceIntDownloadResumeGroupTasks` | `Ys2nTRwMM8M` | +| `sceBgftServiceIntDownloadStartGroupTasks` | `3DeLtzqsmG8` | +| `sceBgftServiceIntDownloadUnregisterCompleteTask` | `TAee3wrD-M0` | +| `sceBgftServiceIntUploadUnregisterCompleteTask` | `I9k+LTS1OlI` | +| `Unknown` | `3cvL6QTobNg` | +| `Unknown` | `4JQKNtsVDpg` | +| `Unknown` | `85axuUkg9eE` | +| `Unknown` | `fJnnIc5sHOU` | +| `Unknown` | `tBOVSvi9EjY` | From 3c9b2011138e76d8ebeebe452613b20419c0b393 Mon Sep 17 00:00:00 2001 From: Kyon Date: Wed, 28 Oct 2026 12:23:24 +0000 Subject: [PATCH 082/160] feat(libSceNpSignaling): declare 15 additional callable exports --- core/libs/prx/libSceNpSignaling/Export.cpp | 65 +++++++++++++++++++ .../include/InventoryExports.hpp | 23 +++++++ .../libSceNpSignaling/libSceNpSignaling.hpp | 1 + docs/dev/debt/libSceNpSignaling.md | 22 +++++++ 4 files changed, 111 insertions(+) create mode 100644 core/libs/prx/libSceNpSignaling/include/InventoryExports.hpp diff --git a/core/libs/prx/libSceNpSignaling/Export.cpp b/core/libs/prx/libSceNpSignaling/Export.cpp index 00f307beb5..c4b7de606b 100644 --- a/core/libs/prx/libSceNpSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSignaling/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceNpSignaling/include/InventoryExports.hpp" #include "prx/libSceNpSignaling/include/Api.hpp" #include #include @@ -171,3 +172,67 @@ int APS5_VABI sceNpSignalingDeactivateConnection(int ctxId, int connId) { } } + +extern "C" { + +int APS5_VABI sceNpSignalingActivateConnectionA() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpSignalingCancelPeerNetInfo() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpSignalingCreateContextA() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpSignalingGetConnectionFromNpId() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpSignalingGetConnectionFromPeerAddressA() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpSignalingGetConnectionFromPeerAddress() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpSignalingGetConnectionInfoA() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpSignalingGetConnectionStatistics() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpSignalingGetConnectionStatus() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpSignalingGetContextOption() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpSignalingGetLocalNetInfo() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpSignalingGetMemoryInfo() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpSignalingGetPeerNetInfoA() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpSignalingGetPeerNetInfoResult() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpSignalingGetPeerNetInfo() { + NotImplemented_nid_no_patch(__func__); +} + +} diff --git a/core/libs/prx/libSceNpSignaling/include/InventoryExports.hpp b/core/libs/prx/libSceNpSignaling/include/InventoryExports.hpp new file mode 100644 index 0000000000..c92c18ea20 --- /dev/null +++ b/core/libs/prx/libSceNpSignaling/include/InventoryExports.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceNpSignalingActivateConnectionA(); +int APS5_VABI sceNpSignalingCancelPeerNetInfo(); +int APS5_VABI sceNpSignalingCreateContextA(); +int APS5_VABI sceNpSignalingGetConnectionFromNpId(); +int APS5_VABI sceNpSignalingGetConnectionFromPeerAddressA(); +int APS5_VABI sceNpSignalingGetConnectionFromPeerAddress(); +int APS5_VABI sceNpSignalingGetConnectionInfoA(); +int APS5_VABI sceNpSignalingGetConnectionStatistics(); +int APS5_VABI sceNpSignalingGetConnectionStatus(); +int APS5_VABI sceNpSignalingGetContextOption(); +int APS5_VABI sceNpSignalingGetLocalNetInfo(); +int APS5_VABI sceNpSignalingGetMemoryInfo(); +int APS5_VABI sceNpSignalingGetPeerNetInfoA(); +int APS5_VABI sceNpSignalingGetPeerNetInfoResult(); +int APS5_VABI sceNpSignalingGetPeerNetInfo(); + +} diff --git a/core/libs/prx/libSceNpSignaling/libSceNpSignaling.hpp b/core/libs/prx/libSceNpSignaling/libSceNpSignaling.hpp index 4ab0459137..a43f2aedab 100644 --- a/core/libs/prx/libSceNpSignaling/libSceNpSignaling.hpp +++ b/core/libs/prx/libSceNpSignaling/libSceNpSignaling.hpp @@ -1,3 +1,4 @@ #pragma once #include "prx/libSceNpSignaling/include/Api.hpp" +#include "prx/libSceNpSignaling/include/InventoryExports.hpp" diff --git a/docs/dev/debt/libSceNpSignaling.md b/docs/dev/debt/libSceNpSignaling.md index 1adbc6177b..11df8ea4d0 100644 --- a/docs/dev/debt/libSceNpSignaling.md +++ b/docs/dev/debt/libSceNpSignaling.md @@ -5,3 +5,25 @@ - [libSceNpSignaling](../../../core/libs/prx/libSceNpSignaling/Export.cpp) (libSceNpSignaling) - offline only, not measured on a PS5: the checks, their order, context ids 1 to 8 and the NpId rule (`term` 0, 3 to 16 characters, the first an ASCII letter or digit, the rest ASCII letters, digits, `_` or `-`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_signaling/np_signaling.cpp#L118-L170) and its [state helpers](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_signaling/np_signaling_state.cpp#L120-L170). `sceNpSignalingInitialize` reserves no heap and starts no thread, so its arguments are not checked, and callbacks are never called. `sceNpSignalingActivateConnection` returns `SCE_NP_ERROR_SIGNED_OUT` after shadPS4's checks up to `OWN_NP_ID`, without its rate limit, and creates no connection, so the connection calls answer `CONN_NOT_FOUND` - [sceNpSignalingDeactivateConnection](../../../core/libs/prx/libSceNpSignaling/Export.cpp) (libSceNpSignaling) - not measured on a PS5: it has the same checks as `sceNpSignalingTerminateConnection` (not initialized, unknown context, then `CONN_NOT_FOUND`), after shadPS4's [call](https://github.com/shadps4-emu/shadPS4/blob/12a4564543ccec527f9723676d2180580d1d7a94/src/core/libraries/np/np_signaling/np_signaling.cpp#L378-L400). No connection is ever created, so none can be deactivated. + +### Additional callable export inventory + +- [Additional callable exports](../../../core/libs/prx/libSceNpSignaling/Export.cpp) - 15 entries have import-resolution declarations only. Parameter lists, return types and behavior are unverified; parameterless `int` declarations are placeholders, not verified ABI contracts. Every entry throws through `NotImplemented_nid_no_patch`. Unidentified names use literal NID aliases. No runtime support, implemented-function increase or title unblock is claimed. + +| Name | NID | +| --- | --- | +| `sceNpSignalingActivateConnectionA` | `ZPLavCKqAB0` | +| `sceNpSignalingCancelPeerNetInfo` | `X1G4kkN2R-8` | +| `sceNpSignalingCreateContextA` | `dDLNFdY8dws` | +| `sceNpSignalingGetConnectionFromNpId` | `GQ0hqmzj0F4` | +| `sceNpSignalingGetConnectionFromPeerAddressA` | `B7cT9aVby7A` | +| `sceNpSignalingGetConnectionFromPeerAddress` | `CkPxQjSm018` | +| `sceNpSignalingGetConnectionInfoA` | `rcylknsUDwg` | +| `sceNpSignalingGetConnectionStatistics` | `C6ZNCDTj00Y` | +| `sceNpSignalingGetConnectionStatus` | `bD-JizUb3JM` | +| `sceNpSignalingGetContextOption` | `npU5V56id34` | +| `sceNpSignalingGetLocalNetInfo` | `U8AQMlOFBc8` | +| `sceNpSignalingGetMemoryInfo` | `tOpqyDyMje4` | +| `sceNpSignalingGetPeerNetInfoA` | `Shr7bZq8QHY` | +| `sceNpSignalingGetPeerNetInfoResult` | `2HajCEGgG4s` | +| `sceNpSignalingGetPeerNetInfo` | `zFgFHId7vAE` | From bdbe6209251648078723ee3c3ac1310282275b4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= Date: Wed, 28 Oct 2026 13:31:31 +0100 Subject: [PATCH 083/160] docs(libSceNpSignaling): drop NID alias claim, none are added --- docs/dev/debt/libSceNpSignaling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNpSignaling.md b/docs/dev/debt/libSceNpSignaling.md index 11df8ea4d0..c838607279 100644 --- a/docs/dev/debt/libSceNpSignaling.md +++ b/docs/dev/debt/libSceNpSignaling.md @@ -8,7 +8,7 @@ ### Additional callable export inventory -- [Additional callable exports](../../../core/libs/prx/libSceNpSignaling/Export.cpp) - 15 entries have import-resolution declarations only. Parameter lists, return types and behavior are unverified; parameterless `int` declarations are placeholders, not verified ABI contracts. Every entry throws through `NotImplemented_nid_no_patch`. Unidentified names use literal NID aliases. No runtime support, implemented-function increase or title unblock is claimed. +- [Additional callable exports](../../../core/libs/prx/libSceNpSignaling/Export.cpp) - 15 entries have import-resolution declarations only. Parameter lists, return types and behavior are unverified; parameterless `int` declarations are placeholders, not verified ABI contracts. Every entry throws through `NotImplemented_nid_no_patch`. No runtime support, implemented-function increase or title unblock is claimed. | Name | NID | | --- | --- | From 092268e83ff4e967642cb2d22fedd37565352a58 Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Wed, 28 Oct 2026 07:35:56 -0500 Subject: [PATCH 084/160] test(libSceSaveData.native): cover invalid directory names in sceSaveDataConvert --- core/libs/tests/GuestSaveDataConvert.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/tests/GuestSaveDataConvert.cpp b/core/libs/tests/GuestSaveDataConvert.cpp index 5833f5029d..fa6737d76b 100644 --- a/core/libs/tests/GuestSaveDataConvert.cpp +++ b/core/libs/tests/GuestSaveDataConvert.cpp @@ -78,7 +78,7 @@ int main() { Check(sceSaveDataGetConvertProgress(&progress) == 0, "get convert progress succeeds after subsequent failed convert"); Check(progress < 1.0f, "convert progress drops below 1.0f after subsequent failed convert"); Check(progress == 0.0f, "convert progress reports 0.0f after subsequent failed convert"); - for (const char* invalid : {"../kept", "", ".", "..", "a\\b", "c:d"}) { + for (const char* invalid : {"../kept", "", ".", "..", "a/b", "a\\b", "c:d"}) { Check(Convert(invalid, std::strlen(invalid) + 1) == SaveDataErrorParameter, std::string("\"") + invalid + "\" is rejected"); } char unterminated[sizeof(SceSaveDataDirName::data)]; From 5a1bd58d901981349d1b5f742fca86cb1ffb8540 Mon Sep 17 00:00:00 2001 From: Kyon Date: Wed, 28 Oct 2026 12:23:24 +0000 Subject: [PATCH 085/160] feat(libSceNpTrophy2): declare 15 additional callable exports --- core/libs/prx/libSceNpTrophy2/Export.cpp | 65 +++++++++++++++++++ .../include/InventoryExports.hpp | 23 +++++++ .../prx/libSceNpTrophy2/libSceNpTrophy2.hpp | 1 + docs/dev/debt/libSceNpTrophy2.md | 22 +++++++ 4 files changed, 111 insertions(+) create mode 100644 core/libs/prx/libSceNpTrophy2/include/InventoryExports.hpp diff --git a/core/libs/prx/libSceNpTrophy2/Export.cpp b/core/libs/prx/libSceNpTrophy2/Export.cpp index 8a46b8a803..31a83cfca0 100644 --- a/core/libs/prx/libSceNpTrophy2/Export.cpp +++ b/core/libs/prx/libSceNpTrophy2/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceNpTrophy2/include/InventoryExports.hpp" #include "prx/libSceNpTrophy2/include/Api.hpp" #include #include @@ -122,3 +123,67 @@ int APS5_VABI sceNpTrophy2SystemRemoveUserData(void) { } } + +extern "C" { + +int APS5_VABI sceNpTrophy2SystemBuildBgImageIconUri() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpTrophy2SystemBuildTitleIconUri() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpTrophy2SystemDestroyHandle() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpTrophy2SystemGetTrophyConf() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpTrophy2SystemGetTrophyData() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpTrophy2SystemGetTrophyGroupData() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpTrophy2SystemGetTrophyGroupDetails() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpTrophy2SystemGetTrophyTitleData() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpTrophy2SystemListAllTitles() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpTrophy2SystemListPlayedTitles() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpTrophy2SystemListTitlesByNpTitleId() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpTrophy2SystemNetSyncTitles() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpTrophy2SystemRemoveUserDataWithServerData() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpTrophy2SystemUnregisterTitleSyncedCallback() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceNpTrophy2SystemUnregisterTitleUpdateCallback() { + NotImplemented_nid_no_patch(__func__); +} + +} diff --git a/core/libs/prx/libSceNpTrophy2/include/InventoryExports.hpp b/core/libs/prx/libSceNpTrophy2/include/InventoryExports.hpp new file mode 100644 index 0000000000..0f4c75ba24 --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/include/InventoryExports.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI sceNpTrophy2SystemBuildBgImageIconUri(); +int APS5_VABI sceNpTrophy2SystemBuildTitleIconUri(); +int APS5_VABI sceNpTrophy2SystemDestroyHandle(); +int APS5_VABI sceNpTrophy2SystemGetTrophyConf(); +int APS5_VABI sceNpTrophy2SystemGetTrophyData(); +int APS5_VABI sceNpTrophy2SystemGetTrophyGroupData(); +int APS5_VABI sceNpTrophy2SystemGetTrophyGroupDetails(); +int APS5_VABI sceNpTrophy2SystemGetTrophyTitleData(); +int APS5_VABI sceNpTrophy2SystemListAllTitles(); +int APS5_VABI sceNpTrophy2SystemListPlayedTitles(); +int APS5_VABI sceNpTrophy2SystemListTitlesByNpTitleId(); +int APS5_VABI sceNpTrophy2SystemNetSyncTitles(); +int APS5_VABI sceNpTrophy2SystemRemoveUserDataWithServerData(); +int APS5_VABI sceNpTrophy2SystemUnregisterTitleSyncedCallback(); +int APS5_VABI sceNpTrophy2SystemUnregisterTitleUpdateCallback(); + +} diff --git a/core/libs/prx/libSceNpTrophy2/libSceNpTrophy2.hpp b/core/libs/prx/libSceNpTrophy2/libSceNpTrophy2.hpp index 09c653d06d..0a98172b4b 100644 --- a/core/libs/prx/libSceNpTrophy2/libSceNpTrophy2.hpp +++ b/core/libs/prx/libSceNpTrophy2/libSceNpTrophy2.hpp @@ -1,3 +1,4 @@ #pragma once #include "prx/libSceNpTrophy2/include/Api.hpp" +#include "prx/libSceNpTrophy2/include/InventoryExports.hpp" diff --git a/docs/dev/debt/libSceNpTrophy2.md b/docs/dev/debt/libSceNpTrophy2.md index 8552c9e0de..9df791cdd7 100644 --- a/docs/dev/debt/libSceNpTrophy2.md +++ b/docs/dev/debt/libSceNpTrophy2.md @@ -19,3 +19,25 @@ - [Trophy progress statistics](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): `max` (the largest value posted, starting from `initialValue`, or `minValue` without one), `int32` (0 to 2147483647) and a rule condition of an `eventName` and one `property` whose `comparator` is `==` and `value` an unsigned integer follow the rules a title ships for its progressive trophies, not a console measurement. Borderlands 4 (PPSA01494) feeds each one's `udsStatId` from `$._trophy_progress` of `_UpdateTrophyProgress`, through an `int32` `max` statistic and a rule on `$._trophy_id`. `latest` and the unsigned types follow KytyPS5. - [Platinum](../../../core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp): an unlock through any path unlocks the platinum that the trophy names in `platinumTrophyId` once every trophy naming that platinum is unlocked, as KytyPS5 does; trophies without `platinumTrophyId` are not required. With the title's trophy data loaded, a posted `_UnlockTrophy` of a trophy that is not in the data, of a platinum or of a progressive trophy is not stored and prints a `[trophy] ignored` line, as KytyPS5 refuses them, so a progressive trophy unlocks only through its statistic; without trophy data any id of 0 or more is stored, including ids the data would make the platinum, a progressive trophy or no trophy, and the platinum is not unlocked when the trophies it needs are. The console's behaviour is not known. - [Damaged records](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp): a line in `_trophy/_.txt` or `.stats.txt` that is not a new ` ` pair (a blank line, a carriage return, a repeated or negative id, a value that is not a number) makes `readRecord` throw a `std::runtime_error` that names the file and the line. Every call that reads that record then throws it instead of returning, which stops the title as any throwing export does, so the title gets no return value; main returned 0 from these calls. The failed read is not kept, so each later call reads the file again and throws again until the file is fixed or deleted. The unlock record is read by `sceNpTrophy2GetTrophyInfo`, `sceNpTrophy2GetTrophyInfoArray` and `sceNpTrophy2GetGameInfo` for a created context, by `sceNpTrophy2GetGroupInfo` and `sceNpTrophy2GetGroupInfoArray` when the title's trophy data is loaded, by a `sceNpUniversalDataSystemPostEvent` of an `_UnlockTrophy` that the trophy data does not refuse, and by a statistic change when the trophy data is loaded; the stat record by a `sceNpUniversalDataSystemPostEvent` of an event that a statistics rule copies and by the trophy queries of a progressive trophy with a statistic. What the console does with a damaged record is not known. + +### Additional callable export inventory + +- [Additional callable exports](../../../core/libs/prx/libSceNpTrophy2/Export.cpp) - 15 entries have import-resolution declarations only. Parameter lists, return types and behavior are unverified; parameterless `int` declarations are placeholders, not verified ABI contracts. Every entry throws through `NotImplemented_nid_no_patch`. Unidentified names use literal NID aliases. No runtime support, implemented-function increase or title unblock is claimed. + +| Name | NID | +| --- | --- | +| `sceNpTrophy2SystemBuildBgImageIconUri` | `UnbjI+AqX+A` | +| `sceNpTrophy2SystemBuildTitleIconUri` | `2dg-TbNmKv8` | +| `sceNpTrophy2SystemDestroyHandle` | `88V2B8r-Agw` | +| `sceNpTrophy2SystemGetTrophyConf` | `4hpE8WXIaHs` | +| `sceNpTrophy2SystemGetTrophyData` | `zod--HLBtbI` | +| `sceNpTrophy2SystemGetTrophyGroupData` | `VW52w-a9TAw` | +| `sceNpTrophy2SystemGetTrophyGroupDetails` | `+9bvXT+gv9E` | +| `sceNpTrophy2SystemGetTrophyTitleData` | `+GPklPyQZVU` | +| `sceNpTrophy2SystemListAllTitles` | `fl5I-yVj7vA` | +| `sceNpTrophy2SystemListPlayedTitles` | `1SJO40vt-xk` | +| `sceNpTrophy2SystemListTitlesByNpTitleId` | `3+DuF1o21D8` | +| `sceNpTrophy2SystemNetSyncTitles` | `pBPp5mpE-iw` | +| `sceNpTrophy2SystemRemoveUserDataWithServerData` | `w2Uq3rRtT9I` | +| `sceNpTrophy2SystemUnregisterTitleSyncedCallback` | `Gqf-wNaD-mM` | +| `sceNpTrophy2SystemUnregisterTitleUpdateCallback` | `-LSu4NCj2p4` | From 7da980327e6ef9e11b13bcad08a594d92bf7f971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= Date: Wed, 28 Oct 2026 13:38:17 +0100 Subject: [PATCH 086/160] docs(libSceNpTrophy2): drop literal NID alias claim, none are added Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceNpTrophy2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNpTrophy2.md b/docs/dev/debt/libSceNpTrophy2.md index 9df791cdd7..aefe298230 100644 --- a/docs/dev/debt/libSceNpTrophy2.md +++ b/docs/dev/debt/libSceNpTrophy2.md @@ -22,7 +22,7 @@ ### Additional callable export inventory -- [Additional callable exports](../../../core/libs/prx/libSceNpTrophy2/Export.cpp) - 15 entries have import-resolution declarations only. Parameter lists, return types and behavior are unverified; parameterless `int` declarations are placeholders, not verified ABI contracts. Every entry throws through `NotImplemented_nid_no_patch`. Unidentified names use literal NID aliases. No runtime support, implemented-function increase or title unblock is claimed. +- [Additional callable exports](../../../core/libs/prx/libSceNpTrophy2/Export.cpp) - 15 entries have import-resolution declarations only. Parameter lists, return types and behavior are unverified; parameterless `int` declarations are placeholders, not verified ABI contracts. Every entry throws through `NotImplemented_nid_no_patch`. No runtime support, implemented-function increase or title unblock is claimed. | Name | NID | | --- | --- | From 90a9ab60c0ad3efb8170b8abc5689fa6c3d53b07 Mon Sep 17 00:00:00 2001 From: Mehran Date: Wed, 28 Oct 2026 15:40:02 +0300 Subject: [PATCH 087/160] test(libs): give the prx loader tests the MinGW runtime on their PATH guest_kernel_sys_threads_main, guest_kernel_sys_threads_main_patched and net_ipv6only_patched link the MinGW runtime statically and load libkernel_sys.prx or libSceNet.prx with LoadLibraryExW and LOAD_WITH_ALTERED_SEARCH_PATH. Windows then looks for the libraries' libstdc++-6.dll next to the .prx and on PATH, not next to the test executable where the build copies the runtime. From Git Bash, whose PATH starts with Git's mingw64/bin, the loader took Git's libstdc++-6.dll, which lacks the std::__codecvt_utf8_utf16_base members that libkernel_sys, libkernel and libc import. The threads tests hung on an "Entry Point Not Found" dialog until their 20 s timeout, and net_ipv6only_patched, which suppresses the dialog, got error 127 and aborted. On MinGW, prepend the compiler's directory to their PATH, as patched_libraries does since #10101. --- core/libs/CMakeLists.txt | 5 +++++ core/libs/prx/libSceNet/CMakeLists.txt | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index dc463eea7f..a234de0e58 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -459,6 +459,11 @@ if(BUILD_TESTING) add_test(NAME guest_kernel_sys_threads_${case} COMMAND guest_kernel_sys_threads_tests ${UNPATCHED_DIR} unpatched ${case}) add_test(NAME guest_kernel_sys_threads_${case}_patched COMMAND guest_kernel_sys_threads_tests ${PATCHED_DIR} patched ${case}) set_tests_properties(guest_kernel_sys_threads_${case} guest_kernel_sys_threads_${case}_patched PROPERTIES TIMEOUT 20) + if(MINGW) + get_filename_component(kernelSysThreadsRuntimeDirectory "${CMAKE_CXX_COMPILER}" DIRECTORY) + set_tests_properties(guest_kernel_sys_threads_${case} guest_kernel_sys_threads_${case}_patched PROPERTIES + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:${kernelSysThreadsRuntimeDirectory}") + endif() endforeach() add_test_executable(guest_gnm_neo_packets_tests tests/GuestGnmNeoPackets.cpp) diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index acc487792e..10ad0db42a 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -16,6 +16,11 @@ if(BUILD_TESTING) endif() add_test(NAME net_ipv6only_patched COMMAND net_ipv6only_patched_tests ${PATCHED_DIR}) set_tests_properties(net_ipv6only_patched PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77) + if(MINGW) + get_filename_component(ipv6OnlyPatchedRuntimeDirectory "${CMAKE_CXX_COMPILER}" DIRECTORY) + set_tests_properties(net_ipv6only_patched PROPERTIES + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:${ipv6OnlyPatchedRuntimeDirectory}") + endif() add_test_executable(net_ipv6only_tests tests/Ipv6Only.cpp) target_include_directories(net_ipv6only_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(net_ipv6only_tests PRIVATE libSceNet libkernel libc) From fcd4f0dc5a020939236d329a23680ed9edde41d6 Mon Sep 17 00:00:00 2001 From: Omar Elmahy Date: Wed, 28 Oct 2026 12:40:10 +0000 Subject: [PATCH 088/160] test(libSceContentSearch): cover post-terminate lifecycle and argument precedence --- core/libs/tests/GuestContentSearch.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/libs/tests/GuestContentSearch.cpp b/core/libs/tests/GuestContentSearch.cpp index 1adc3da4a6..7101d21c48 100644 --- a/core/libs/tests/GuestContentSearch.cpp +++ b/core/libs/tests/GuestContentSearch.cpp @@ -133,6 +133,13 @@ int main() { Require(sceContentSearchCloseMetadata(1) == ErrorInvalidArgument); Require(sceContentSearchTerm() == 0); + Require(sceContentSearchTerm() == ErrorNotInitialized); + Require(sceContentSearchOpenMetadata(nullptr, nullptr) == ErrorNotInitialized); + Require(sceContentSearchOpenMetadataByContentId(1, nullptr) == ErrorNotInitialized); + Require(sceContentSearchGetTotalContentSize(&columnSet, 1, &size, &updateId) == ErrorNotInitialized); + Require(sceContentSearchGetNumOfContent(&columnSet, 1, &count, &updateId) == ErrorNotInitialized); + Require(sceContentSearchSearchContent(&columnSet, 1, &orderBy, 1, 0, 2, &count, infos, &updateId) == ErrorNotInitialized); + Require(sceContentSearchCloseMetadata(1) == ErrorNotInitialized); Require(sceContentSearchGetContentLastUpdateId(&updateId) == ErrorNotInitialized); Require(updateId == -1); constexpr int binderResult = static_cast(0x809D000F); From b28edaf954d974ad254849142d8a0beb44609b96 Mon Sep 17 00:00:00 2001 From: cadenasynyster <8373765+cadenasynyster@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:54:45 +0200 Subject: [PATCH 089/160] fix(libkernel): rmdir and unlink on a Windows directory symbolic link On Windows, std::filesystem::symlink_status and is_symlink report a directory symbolic link as a plain directory, so rmdir removed the link instead of failing with ENOTDIR (20) and unlink failed with EPERM (1) instead of removing it, unlike FreeBSD. Detect the symlink reparse tag with FindFirstFileW: rmdir returns ENOTDIR, unlink removes the link with RemoveDirectoryW. Junctions and volume mounts keep their previous behaviour. Enable the directory-link and dangling-link blocks of guest_filesystem on Windows (created with CreateSymbolicLinkW). AI-assisted: yes Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01GtXavMkgCTCdmRpweviHcQ --- core/libs/prx/libkernel/File/src/Open.cpp | 11 ++++++++ core/libs/tests/GuestFilesystem.cpp | 32 ++++++++++++++++++++--- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 13bfaf43b8..3b0a3bbd27 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -490,6 +490,17 @@ int APS5_VABI sceKernelUnlink(const char* path) { std::error_code error; if (std::filesystem::is_directory(std::filesystem::symlink_status(native, error))) { #ifdef _WIN32 + if (native.has_filename()) { + WIN32_FIND_DATAW found{}; + if (const HANDLE find = FindFirstFileW(native.c_str(), &found); find != INVALID_HANDLE_VALUE) { + FindClose(find); + if ((found.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0 && found.dwReserved0 == IO_REPARSE_TAG_SYMLINK) { + if (!RemoveDirectoryW(native.c_str())) return SceErrorFromErrno(GetLastError() == ERROR_ACCESS_DENIED ? 13 : 5); + RecordWrittenPath_nid_no_patch(native); + return 0; + } + } + } const DWORD attributes = GetFileAttributesW(native.c_str()); if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { std::string junctionTarget; diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index c854d40b34..1d9c774207 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -858,11 +858,22 @@ int main() { Require(replace() == 0 && !std::filesystem::exists(incoming) && contents() == "replaced"); Require(remove_nid_postfix(occupied.c_str()) == 0); } -#ifndef _WIN32 const auto dangling = area / "dangling"; + const auto replacement = area / "replacement"; +#ifdef _WIN32 + if (CreateSymbolicLinkW(dangling.c_str(), (area / "nonexistent").c_str(), SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) == 0) { + Require(GetLastError() == ERROR_PRIVILEGE_NOT_HELD); + std::fprintf(stderr, "Windows dangling link rename skipped: creating symbolic links needs Developer Mode or administrator rights\n"); + } else { + Require((GetFileAttributesW(dangling.c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) != 0); + { std::ofstream stream(replacement); stream << "replacement"; } + Require(rename_nid_postfix(name(replacement).c_str(), name(dangling).c_str()) == 0); + const DWORD replaced = GetFileAttributesW(dangling.c_str()); + Require(replaced != INVALID_FILE_ATTRIBUTES && (replaced & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY)) == 0); + } +#else std::filesystem::create_symlink(area / "nonexistent", dangling); Require(std::filesystem::is_symlink(dangling)); - const auto replacement = area / "replacement"; { std::ofstream stream(replacement); stream << "replacement"; } Require(rename_nid_postfix(name(replacement).c_str(), name(dangling).c_str()) == 0); Require(!std::filesystem::is_symlink(dangling) && std::filesystem::is_regular_file(dangling)); @@ -1493,11 +1504,24 @@ int main() { Require(fopen_nid_postfix(blocked->c_str(), openMode) == nullptr && *__error_nid_postfix() == expected); } } -#ifndef _WIN32 const auto linkedDirectory = root / "linked-directory"; const auto directoryLink = root / "directory-link"; const auto danglingDirectoryLink = root / "dangling-directory-link"; Require(std::filesystem::create_directory(linkedDirectory)); +#ifdef _WIN32 + constexpr DWORD DirectoryLinkFlags = SYMBOLIC_LINK_FLAG_DIRECTORY | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; + if (CreateSymbolicLinkW(directoryLink.c_str(), L"linked-directory", DirectoryLinkFlags) == 0) { + Require(GetLastError() == ERROR_PRIVILEGE_NOT_HELD); + std::fprintf(stderr, "Windows directory link checks skipped: creating symbolic links needs Developer Mode or administrator rights\n"); + } else { + Require(CreateSymbolicLinkW(danglingDirectoryLink.c_str(), L"missing-directory", DirectoryLinkFlags) != 0); + Require(rmdir_nid_postfix(directoryLink.string().c_str()) == -1 && *__error_nid_postfix() == 20); + Require(sceKernelRmdir(directoryLink.string().c_str()) == static_cast(0x80020014u)); + Require(rmdir_nid_postfix(danglingDirectoryLink.string().c_str()) == -1 && *__error_nid_postfix() == 20); + Require((GetFileAttributesW(directoryLink.c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) != 0 && std::filesystem::is_directory(linkedDirectory)); + Require(unlink_nid_postfix(directoryLink.string().c_str()) == 0 && unlink_nid_postfix(danglingDirectoryLink.string().c_str()) == 0); + } +#else std::filesystem::create_directory_symlink("linked-directory", directoryLink); std::filesystem::create_directory_symlink("missing-directory", danglingDirectoryLink); Require(rmdir_nid_postfix(directoryLink.string().c_str()) == -1 && *__error_nid_postfix() == 20); @@ -1505,8 +1529,8 @@ int main() { Require(rmdir_nid_postfix(danglingDirectoryLink.string().c_str()) == -1 && *__error_nid_postfix() == 20); Require(std::filesystem::is_symlink(directoryLink) && std::filesystem::is_directory(linkedDirectory)); Require(unlink_nid_postfix(directoryLink.string().c_str()) == 0 && unlink_nid_postfix(danglingDirectoryLink.string().c_str()) == 0); - Require(rmdir_nid_postfix(linkedDirectory.string().c_str()) == 0 && !std::filesystem::exists(linkedDirectory)); #endif + Require(rmdir_nid_postfix(linkedDirectory.string().c_str()) == 0 && !std::filesystem::exists(linkedDirectory)); const auto dotDirectory = root / "dot-directory"; Require(std::filesystem::create_directory(dotDirectory)); const auto dotDirectoryName = dotDirectory.string() + "/."; From ad3a0496c61c382ea93eee200c93c0c54a83af44 Mon Sep 17 00:00:00 2001 From: Omar Elmahy Date: Wed, 28 Oct 2026 12:44:24 +0000 Subject: [PATCH 090/160] test(libSceRandom): cover maximum size boundary and unsigned overflow validation --- core/libs/tests/GuestRandom.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/libs/tests/GuestRandom.cpp b/core/libs/tests/GuestRandom.cpp index 6361b6b4ab..661667f77f 100644 --- a/core/libs/tests/GuestRandom.cpp +++ b/core/libs/tests/GuestRandom.cpp @@ -1,6 +1,7 @@ #include "prx/libSceRandom/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include @@ -12,14 +13,20 @@ int main() { Require(sceRandomGetRandomNumber(nullptr, 0) == invalid); Require(sceRandomGetRandomNumber(nullptr, 64) == invalid); Require(sceRandomGetRandomNumber(nullptr, 65) == invalid); + Require(sceRandomGetRandomNumber(nullptr, SIZE_MAX) == invalid); std::memset(buffer, 0xAA, sizeof(buffer)); Require(sceRandomGetRandomNumber(buffer, 65) == invalid); + Require(sceRandomGetRandomNumber(buffer, 66) == invalid); + Require(sceRandomGetRandomNumber(buffer, 128) == invalid); + Require(sceRandomGetRandomNumber(buffer, SIZE_MAX) == invalid); Require(buffer[0] == 0xAA); Require(sceRandomGetRandomNumber(buffer, 0) == 0); Require(buffer[0] == 0xAA); Require(sceRandomGetRandomNumber(buffer, 1) == 0); Require(sceRandomGetRandomNumber(buffer, 7) == 0); Require(buffer[7] == 0xAA); + unsigned char maxBuf[64] = {}; + Require(sceRandomGetRandomNumber(maxBuf, 64) == 0); unsigned char first[64] = {}; unsigned char second[64] = {}; Require(sceRandomGetRandomNumber(first, sizeof(first)) == 0); From a893c2caa277096106845304a8fa3c8354b41478 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:46:24 +0200 Subject: [PATCH 091/160] feat(libSceGameLiveStreaming): answer offline setting and feedback calls locally --- .../prx/libSceGameLiveStreaming/Export.cpp | 115 +++++++++++++----- .../libSceGameLiveStreaming/include/Api.hpp | 53 +++++--- docs/dev/debt/libSceGameLiveStreaming.md | 20 +-- 3 files changed, 128 insertions(+), 60 deletions(-) diff --git a/core/libs/prx/libSceGameLiveStreaming/Export.cpp b/core/libs/prx/libSceGameLiveStreaming/Export.cpp index d9e1f9a227..ec6278d1e3 100644 --- a/core/libs/prx/libSceGameLiveStreaming/Export.cpp +++ b/core/libs/prx/libSceGameLiveStreaming/Export.cpp @@ -15,6 +15,27 @@ static constexpr int ErrorInvalidArgument = static_cast(0x80A00002); static constexpr int ErrorAlreadyInitialized = static_cast(0x80A00003); static constexpr int ErrorNotInitialized = static_cast(0x80A00004); static constexpr std::size_t ProgramInfoSize = 0x134C; +static constexpr std::size_t MaxMetadataLength = 240; +static constexpr std::size_t MaxPresetDescriptionLength = 720; +static constexpr std::size_t MaxInvitationSessionIdLength = 45; +static constexpr std::size_t MaxSocialMessageLength = 360; +static constexpr std::size_t MaxSpoilerTags = 16; +static constexpr std::size_t MaxPresetCommands = 5; + +static bool ValidFeedbackType(int type) { + return type >= 1 && type <= 3; +} + +static bool ValidString(const char* text, std::size_t maxLength) { + return text != nullptr && std::memchr(text, 0, maxLength + 1) != nullptr; +} + +static bool AllZero(const std::uint8_t* bytes, std::size_t size) { + for (std::size_t i = 0; i < size; i++) { + if (bytes[i] != 0) return false; + } + return true; +} static_assert(sizeof(GameLiveStreamingStatus2) == 72); @@ -87,13 +108,15 @@ int APS5_VABI sceGameLiveStreamingScreenSetMode(void) { return 0; } -int APS5_VABI sceGameLiveStreamingPermitLiveStreaming(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGameLiveStreamingPermitLiveStreaming(bool permit) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return ErrorNotInitialized; return 0; } -int APS5_VABI sceGameLiveStreamingSetMaxBitrate(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGameLiveStreamingSetMaxBitrate(std::uint32_t bitrate) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return ErrorNotInitialized; return 0; } @@ -102,68 +125,98 @@ int APS5_VABI sceGameLiveStreamingGetCurrentStatus(GameLiveStreamingStatus* stat return 0; } -int APS5_VABI sceGameLiveStreamingEnableSocialFeedback(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGameLiveStreamingEnableSocialFeedback(bool enable) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return ErrorNotInitialized; return 0; } -int APS5_VABI sceGameLiveStreamingGetSocialFeedbackMessagesCount(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGameLiveStreamingGetSocialFeedbackMessagesCount(int type, std::uint32_t* count) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return ErrorNotInitialized; + if (count == nullptr || !ValidFeedbackType(type)) return ErrorInvalidArgument; + *count = 0; return 0; } -int APS5_VABI sceGameLiveStreamingGetSocialFeedbackMessages(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGameLiveStreamingGetSocialFeedbackMessages(int type, size_t capacity, GameLiveStreamingFeedbackMessage* messages, size_t* acquired) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return ErrorNotInitialized; + if (messages == nullptr || acquired == nullptr || !ValidFeedbackType(type)) return ErrorInvalidArgument; + *acquired = 0; return 0; } -int APS5_VABI sceGameLiveStreamingPermitServerSideRecording(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGameLiveStreamingPermitServerSideRecording(bool permit) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return ErrorNotInitialized; return 0; } -int APS5_VABI sceGameLiveStreamingSetMetadata(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGameLiveStreamingSetMetadata(const char* metadata) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return ErrorNotInitialized; + if (!ValidString(metadata, MaxMetadataLength)) return ErrorInvalidArgument; return 0; } -int APS5_VABI sceGameLiveStreamingSetSpoilerTag(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGameLiveStreamingSetSpoilerTag(size_t count, const GameLiveStreamingSpoilerTag* tags) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return ErrorNotInitialized; + if (tags == nullptr || count > MaxSpoilerTags) return ErrorInvalidArgument; + for (size_t i = 0; i < count; i++) { + if (!AllZero(tags[i].reserved, sizeof(tags[i].reserved))) return ErrorInvalidArgument; + } return 0; } int APS5_VABI sceGameLiveStreamingClearSpoilerTag(void) { - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(g_mutex); + if (!g_initialized) return ErrorNotInitialized; return 0; } -int APS5_VABI sceGameLiveStreamingSetPresetSocialFeedbackCommands(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGameLiveStreamingSetPresetSocialFeedbackCommands(size_t count, const GameLiveStreamingPresetCommand* commands) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return ErrorNotInitialized; + if (commands == nullptr || count > MaxPresetCommands) return ErrorInvalidArgument; + for (size_t i = 0; i < count; i++) { + if (!AllZero(commands[i].reserved, sizeof(commands[i].reserved))) return ErrorInvalidArgument; + } return 0; } int APS5_VABI sceGameLiveStreamingClearPresetSocialFeedbackCommands(void) { - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(g_mutex); + if (!g_initialized) return ErrorNotInitialized; return 0; } -int APS5_VABI sceGameLiveStreamingSetPresetSocialFeedbackCommandsDescription(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGameLiveStreamingSetPresetSocialFeedbackCommandsDescription(const char* description) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return ErrorNotInitialized; + if (!ValidString(description, MaxPresetDescriptionLength)) return ErrorInvalidArgument; return 0; } -int APS5_VABI sceGameLiveStreamingClearSocialFeedbackMessages(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGameLiveStreamingClearSocialFeedbackMessages(int type) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return ErrorNotInitialized; + if (!ValidFeedbackType(type)) return ErrorInvalidArgument; return 0; } -int APS5_VABI sceGameLiveStreamingPostSocialMessage(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGameLiveStreamingPostSocialMessage(const char* message) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return ErrorNotInitialized; + if (!ValidString(message, MaxSocialMessageLength)) return ErrorInvalidArgument; return 0; } -int APS5_VABI sceGameLiveStreamingSetCameraFrameSetting(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGameLiveStreamingSetCameraFrameSetting(const GameLiveStreamingCameraFrameSetting* setting) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return ErrorNotInitialized; + if (setting == nullptr || !AllZero(setting->reserved, sizeof(setting->reserved))) return ErrorInvalidArgument; return 0; } @@ -177,8 +230,10 @@ int APS5_VABI sceGameLiveStreamingSetDefaultServiceProviderPermission(void) { return 0; } -int APS5_VABI sceGameLiveStreamingSetInvitationSessionId(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceGameLiveStreamingSetInvitationSessionId(const char* sessionId) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return ErrorNotInitialized; + if (!ValidString(sessionId, MaxInvitationSessionIdLength)) return ErrorInvalidArgument; return 0; } diff --git a/core/libs/prx/libSceGameLiveStreaming/include/Api.hpp b/core/libs/prx/libSceGameLiveStreaming/include/Api.hpp index 28b744e492..9f69bbc0fc 100644 --- a/core/libs/prx/libSceGameLiveStreaming/include/Api.hpp +++ b/core/libs/prx/libSceGameLiveStreaming/include/Api.hpp @@ -6,6 +6,31 @@ struct GameLiveStreamingProgramInfo; +struct GameLiveStreamingSpoilerTag { + char text[193]; + std::uint8_t reserved[31]; +}; + +static_assert(offsetof(GameLiveStreamingSpoilerTag, reserved) == 193); +static_assert(sizeof(GameLiveStreamingSpoilerTag) == 224); + +struct GameLiveStreamingPresetCommand { + std::uint8_t body[197]; + std::uint8_t reserved[31]; +}; + +static_assert(offsetof(GameLiveStreamingPresetCommand, reserved) == 197); +static_assert(sizeof(GameLiveStreamingPresetCommand) == 228); + +struct GameLiveStreamingCameraFrameSetting { + std::uint8_t head[9]; + std::uint8_t reserved[31]; +}; + +static_assert(offsetof(GameLiveStreamingCameraFrameSetting, reserved) == 9); + +struct GameLiveStreamingFeedbackMessage; + struct GameLiveStreamingStatus { std::uint8_t opaque00[12]; std::uint8_t reserved[60]; @@ -38,25 +63,25 @@ int APS5_VABI sceGameLiveStreamingScreenCloseSeparateMode(void); int APS5_VABI sceGameLiveStreamingScreenConfigureSeparateMode(void); int APS5_VABI sceGameLiveStreamingScreenOpenSeparateMode(void); int APS5_VABI sceGameLiveStreamingScreenSetMode(void); -int APS5_VABI sceGameLiveStreamingPermitLiveStreaming(void); -int APS5_VABI sceGameLiveStreamingSetMaxBitrate(void); +int APS5_VABI sceGameLiveStreamingPermitLiveStreaming(bool permit); +int APS5_VABI sceGameLiveStreamingSetMaxBitrate(std::uint32_t bitrate); int APS5_VABI sceGameLiveStreamingGetCurrentStatus(GameLiveStreamingStatus* status); -int APS5_VABI sceGameLiveStreamingEnableSocialFeedback(void); -int APS5_VABI sceGameLiveStreamingGetSocialFeedbackMessagesCount(void); -int APS5_VABI sceGameLiveStreamingGetSocialFeedbackMessages(void); -int APS5_VABI sceGameLiveStreamingPermitServerSideRecording(void); -int APS5_VABI sceGameLiveStreamingSetMetadata(void); -int APS5_VABI sceGameLiveStreamingSetSpoilerTag(void); +int APS5_VABI sceGameLiveStreamingEnableSocialFeedback(bool enable); +int APS5_VABI sceGameLiveStreamingGetSocialFeedbackMessagesCount(int type, std::uint32_t* count); +int APS5_VABI sceGameLiveStreamingGetSocialFeedbackMessages(int type, size_t capacity, GameLiveStreamingFeedbackMessage* messages, size_t* acquired); +int APS5_VABI sceGameLiveStreamingPermitServerSideRecording(bool permit); +int APS5_VABI sceGameLiveStreamingSetMetadata(const char* metadata); +int APS5_VABI sceGameLiveStreamingSetSpoilerTag(size_t count, const GameLiveStreamingSpoilerTag* tags); int APS5_VABI sceGameLiveStreamingClearSpoilerTag(void); -int APS5_VABI sceGameLiveStreamingSetPresetSocialFeedbackCommands(void); +int APS5_VABI sceGameLiveStreamingSetPresetSocialFeedbackCommands(size_t count, const GameLiveStreamingPresetCommand* commands); int APS5_VABI sceGameLiveStreamingClearPresetSocialFeedbackCommands(void); -int APS5_VABI sceGameLiveStreamingSetPresetSocialFeedbackCommandsDescription(void); -int APS5_VABI sceGameLiveStreamingClearSocialFeedbackMessages(void); -int APS5_VABI sceGameLiveStreamingPostSocialMessage(void); -int APS5_VABI sceGameLiveStreamingSetCameraFrameSetting(void); +int APS5_VABI sceGameLiveStreamingSetPresetSocialFeedbackCommandsDescription(const char* description); +int APS5_VABI sceGameLiveStreamingClearSocialFeedbackMessages(int type); +int APS5_VABI sceGameLiveStreamingPostSocialMessage(const char* message); +int APS5_VABI sceGameLiveStreamingSetCameraFrameSetting(const GameLiveStreamingCameraFrameSetting* setting); int APS5_VABI sceGameLiveStreamingSetServiceProviderPermission(void); int APS5_VABI sceGameLiveStreamingSetDefaultServiceProviderPermission(void); -int APS5_VABI sceGameLiveStreamingSetInvitationSessionId(void); +int APS5_VABI sceGameLiveStreamingSetInvitationSessionId(const char* sessionId); int APS5_VABI sceGameLiveStreamingStartGenerateStandbyScreenResource(void); int APS5_VABI sceGameLiveStreamingStopGenerateStandbyScreenResource(void); int APS5_VABI sceGameLiveStreamingRegisterCallback(void); diff --git a/docs/dev/debt/libSceGameLiveStreaming.md b/docs/dev/debt/libSceGameLiveStreaming.md index c0cc7bcb4a..b0aacd6a98 100644 --- a/docs/dev/debt/libSceGameLiveStreaming.md +++ b/docs/dev/debt/libSceGameLiveStreaming.md @@ -7,6 +7,10 @@ - [sceGameLiveStreamingGetCurrentStatus2](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) - the supplied module confirms the 72-byte layout and inactive status (user -1, remaining bytes zero). Live service counters, active-user changes and broadcast-resolution conversion are unimplemented. In the 25 instructions after their one call, PPSA01494, PPSA30839 and PPSA31162 read `isOnAir` (the byte at 4) and `spectatorCounts` (the word at 8), so those two are the fields these titles act on; here they stay false and 0. - [sceGameLiveStreamingGetProgramInfo](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - the PS5 13.60 module clears the 0x134C-byte program info, asks the system service for its first 0xFD4 bytes and returns `0x80A00002` for a null pointer and `0x80A00004` before initialization. With nothing broadcast here the info stays zero, which assumes the service reports an empty program when nothing is on air; the field layout is unknown. Two titles (PPSA03001 at 0x1ad86b4, PPSA07426 at 0xe89594) disassembled for this entry pass the address of a stack buffer with no store into it between the start of the block and the call; for a result other than 0 they do not read it, for 0 they copy three text fields at offsets 0, 0x802 and 0xC03, and they do not tell `0x80A00002` from `0x80A00004`. The buffer size is not shown. +### Silent stubs + +- [sceGameLiveStreamingPermitLiveStreaming](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) - this and fifteen related setting, feedback and message calls (PermitServerSideRecording, SetMaxBitrate, EnableSocialFeedback, SetMetadata, Set/ClearSpoilerTag, Set/ClearPresetSocialFeedbackCommands, ...Description, SetCameraFrameSetting, SetInvitationSessionId, Get/ClearSocialFeedbackMessages, GetSocialFeedbackMessagesCount, PostSocialMessage) validate their arguments locally and store nothing, as no broadcast is ever on air. Order: not initialized (`0x80A00004`), then `0x80A00002` for a null pointer (including a null array with count 0 for SetSpoilerTag and a null `messages` with capacity 0), a feedback type outside 1-3, a string longer than 240 (SetMetadata), 720 (preset description), 45 (invitation session id) or 360 (PostSocialMessage) characters, more than 16 spoiler tags or 5 preset commands, and a non-zero reserved tail (31 bytes at offset 193 of the 224-byte spoiler tag, at offset 197 of the 228-byte preset command, at offset 9 of the camera setting). These checks and layouts come from a report on this pull request that ran each function of the firmware 13.42 `libSceGameLiveStreaming.sprx` under an emulator with the service call stubbed; the order between the argument checks, the camera setting size and the preset and message field layouts beyond the reserved tails are not measured. What the service returns after validation is not measured on a PS5: the calls return 0 and queries report zero messages. + ### Unknown function info - [sceGameLiveStreamingScreenInitialize](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature @@ -16,25 +20,9 @@ - [sceGameLiveStreamingScreenConfigureSeparateMode](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature - [sceGameLiveStreamingScreenOpenSeparateMode](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature - [sceGameLiveStreamingScreenSetMode](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature -- [sceGameLiveStreamingPermitLiveStreaming](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature -- [sceGameLiveStreamingSetMaxBitrate](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature - [sceGameLiveStreamingGetCurrentStatus](../../../core/libs/prx/libSceGameLiveStreaming/include/Api.hpp) - still throws; the meaning, division and SDK types of the first 12 bytes remain unknown. In the supplied `libSceGameLiveStreaming.sprx`, export `CoPMx369EqM` at 0x1810 takes one output pointer, clears 72 bytes, obtains service command 9's status, copies its first 12 bytes and clears bytes 0x0C..0x47. `GameLiveStreamingStatus` exposes this byte storage without assuming the `GetCurrentStatus2` field layout or SDK alignment. The decompiler's extra SIMD parameter is not an input: the assembly initializes the register before use. -- [sceGameLiveStreamingEnableSocialFeedback](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature -- [sceGameLiveStreamingGetSocialFeedbackMessagesCount](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature -- [sceGameLiveStreamingGetSocialFeedbackMessages](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature -- [sceGameLiveStreamingPermitServerSideRecording](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature -- [sceGameLiveStreamingSetMetadata](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature -- [sceGameLiveStreamingSetSpoilerTag](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature -- [sceGameLiveStreamingClearSpoilerTag](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature -- [sceGameLiveStreamingSetPresetSocialFeedbackCommands](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature -- [sceGameLiveStreamingClearPresetSocialFeedbackCommands](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature -- [sceGameLiveStreamingSetPresetSocialFeedbackCommandsDescription](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature -- [sceGameLiveStreamingClearSocialFeedbackMessages](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature -- [sceGameLiveStreamingPostSocialMessage](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature -- [sceGameLiveStreamingSetCameraFrameSetting](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature - [sceGameLiveStreamingSetServiceProviderPermission](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature - [sceGameLiveStreamingSetDefaultServiceProviderPermission](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature -- [sceGameLiveStreamingSetInvitationSessionId](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature - [sceGameLiveStreamingStartGenerateStandbyScreenResource](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature - [sceGameLiveStreamingStopGenerateStandbyScreenResource](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature - [sceGameLiveStreamingRegisterCallback](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature From 4c77b6ab65ef797d2f804c90607f70c60b406a05 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 22 Oct 2026 03:40:26 +0100 Subject: [PATCH 092/160] feat(libkernel): support EV_CLEAR on a kqueue watched by another kqueue FreeBSD re-evaluates a kqueue's EVFILT_READ note from kqueue_wakeup, which runs whenever the watched kqueue queues a note, so an EV_CLEAR note fires again only after such an activation. The equeue now counts activations, and the EV_CLEAR note fires when the count moves while notes are queued. --- core/libs/prx/libkernel/Equeue/Equeue.cpp | 20 ++++++++++ core/libs/prx/libkernel/Equeue/Equeue.hpp | 2 + core/libs/prx/libkernel/Equeue/src/Kqueue.cpp | 40 +++++++++++++++++-- core/libs/tests/GuestKqueue.cpp | 33 ++++++++++++++- docs/dev/debt/libkernel.md | 2 +- 5 files changed, 91 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel/Equeue/Equeue.cpp b/core/libs/prx/libkernel/Equeue/Equeue.cpp index c3d1691407..207774faa4 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.cpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.cpp @@ -125,6 +125,7 @@ void KernelEqueuePrivate::TriggerExpiredTimers(uint64_t nowNs) { ev.event.data += static_cast(count); ev.deadlineNs += count * ev.intervalNs; } + if (!ev.triggered) ++m_activations; ev.triggered = true; } } @@ -150,7 +151,9 @@ bool KernelEqueuePrivate::PollEvents() { bool polled = false; for (auto& ev : m_events) { if (ev.enabled && ev.filter.pollFunc != nullptr) { + const bool was = ev.triggered; ev.triggered = ev.filter.pollFunc(&ev); + if (!was && ev.triggered) ++m_activations; polled = true; } } @@ -333,6 +336,7 @@ int KernelEqueuePrivate::CollectTriggered(KernelEvent* ev, int num) { e.event = e.pendingEvents.front(); e.pendingEvents.pop_front(); e.triggered = true; + ++m_activations; } const auto next = std::next(it); if (erase) { @@ -416,6 +420,11 @@ std::size_t KernelEqueuePrivate::CountTriggeredEvents() { return static_cast(std::count_if(m_events.begin(), m_events.end(), [](const KernelEqueueEvent& event) { return event.enabled && event.triggered; })); } +std::uint64_t KernelEqueuePrivate::Activations() { + std::unique_lock lock(m_mutex); + return m_activations; +} + std::vector> KernelEqueuePrivate::WatchedQueues() { std::unique_lock lock(m_mutex); std::vector> queues; @@ -501,6 +510,7 @@ int KernelEqueuePrivate::AddEvent(const KernelEqueueEvent& event) { } } else { m_events.push_back(event); + if (event.triggered) ++m_activations; } m_cond.NotifyOne(); WakePollers(); @@ -521,11 +531,13 @@ int KernelEqueuePrivate::TriggerEvent(uintptr_t ident, int16_t filter, void* tri return SCE_KERNEL_ERROR_ENOENT; } if (it->enabled) { + const bool was = it->triggered; if (it->filter.triggerFunc != nullptr) { it->filter.triggerFunc(&*it, triggerData); } else { it->triggered = true; } + if (!was && it->triggered) ++m_activations; } m_cond.NotifyOne(); WakePollers(); @@ -555,6 +567,14 @@ int KernelEqueuePrivate::EditEvent(uintptr_t ident, int16_t filter, const std::f if (it == m_events.end()) { return SCE_KERNEL_ERROR_ENOENT; } + struct Activation { + KernelEqueuePrivate& queue; + KernelEqueueEvent& event; + bool active; + ~Activation() { + if (!active && event.enabled && event.triggered) ++queue.m_activations; + } + } activation{*this, *it, it->enabled && it->triggered}; edit(*it); m_cond.NotifyOne(); WakePollers(); diff --git a/core/libs/prx/libkernel/Equeue/Equeue.hpp b/core/libs/prx/libkernel/Equeue/Equeue.hpp index 4cace734bd..6c924922b1 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.hpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.hpp @@ -92,6 +92,7 @@ public: int GetTriggeredEvents(KernelEvent* ev, int num); bool HasTriggeredEvents(); std::size_t CountTriggeredEvents(); + std::uint64_t Activations(); std::vector> WatchedQueues(); int WaitForEvents(KernelEvent* ev, int num, uint32_t micros); int WaitForEventsUntil(KernelEvent* ev, int num, std::uint64_t deadlineNanos); @@ -124,6 +125,7 @@ private: KernelEqueue m_handle; bool m_reportOncePerWait; bool m_closed = false; + std::uint64_t m_activations = 0; }; using KernelEqueueRef = std::shared_ptr; diff --git a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp index eea3d985ab..2907358440 100644 --- a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp +++ b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp @@ -228,6 +228,32 @@ bool PollKqueue(KernelEqueueEvent* e) { return pending != 0; } +struct ClearedKqueueNote { + std::uint64_t seen = 0; + bool active = false; +}; + +bool PollClearedKqueue(KernelEqueueEvent* e) { + auto& note = *static_cast(e->filter.data); + const auto pending = e->filter.watched->CountTriggeredEvents(); + const auto activations = e->filter.watched->Activations(); + if (activations != note.seen) { + note.seen = activations; + if (pending != 0) { + note.active = true; + e->event.data = static_cast(pending); + } + } + return note.active; +} + +void ResetClearedKqueue(KernelEqueueEvent* e) { + static_cast(e->filter.data)->active = false; + e->triggered = false; + e->event.fflags = 0; + e->event.data = 0; +} + bool KqueueReaches(const KernelEqueueRef& from, const KernelEqueuePrivate* target, std::vector& visited) { if (from.get() == target) { return true; @@ -245,9 +271,6 @@ bool KqueueReaches(const KernelEqueueRef& from, const KernelEqueuePrivate* targe } int AddKqueueKnote(const KernelEqueueRef& queue, const KernelEqueueRef& watched, KernelEvent& change, std::uint16_t flags) { - if ((flags & EV_CLEAR) != 0) { - NotImplemented_nid_no_patch("kevent: EV_CLEAR on a kqueue descriptor"); - } std::lock_guard lock(g_kqueueWatchMutex); std::vector visited; if (KqueueReaches(watched, queue.get(), visited)) { @@ -260,6 +283,17 @@ int AddKqueueKnote(const KernelEqueueRef& queue, const KernelEqueueRef& watched, event.event.data = 0; event.filter.pollFunc = PollKqueue; event.filter.watched = watched; + if ((flags & EV_CLEAR) != 0) { + auto note = std::make_shared(); + const auto pending = watched->CountTriggeredEvents(); + note->seen = watched->Activations(); + note->active = pending != 0; + event.event.data = static_cast(pending); + event.filter.data = note.get(); + event.filter.owner = std::move(note); + event.filter.pollFunc = PollClearedKqueue; + event.filter.resetFunc = ResetClearedKqueue; + } change.fflags = 0; change.data = 0; return GuestErrno(queue->AddEvent(event)); diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index 7b81a77b4a..55644153b3 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -570,8 +570,8 @@ static void WatchKqueue() { RequireFailure(Apply(outer, Change(watched, EvfiltWrite, EvAdd)), GuestEinval); RequireFailure(Apply(outer, Change(watched, EvfiltVnode, EvAdd, NoteWrite)), GuestEinval); - Require(ThrownMessage([outer, watched] { Apply(outer, Change(watched, EvfiltRead, EvAdd | EvClear)); }).find("EV_CLEAR on a kqueue descriptor") != std::string::npos); - RequireFailure(Apply(outer, Change(watched, EvfiltRead, EvDelete)), GuestEnoent); + Require(Apply(outer, Change(watched, EvfiltRead, EvAdd | EvClear)) == 0); + Require(Apply(outer, Change(watched, EvfiltRead, EvDelete)) == 0); Require(Apply(outer, Change(watched, EvfiltRead, EvAdd)) == 0); Require(ThrownMessage([inner, outer] { Apply(inner, Change(static_cast(outer), EvfiltRead, EvAdd)); }).find("watches the kqueue") != std::string::npos); Require(Apply(inner, Change(5, EvfiltUser, 0, NoteTrigger)) == 0); @@ -583,6 +583,34 @@ static void WatchKqueue() { Require(close_nid_postfix(outer) == 0); } +static void ClearedKqueue() { + const int inner = kqueue_nid_postfix(); + const int outer = kqueue_nid_postfix(); + Require(inner >= 0 && outer >= 0); + const auto watched = static_cast(inner); + KernelEvent events[4]{}; + Require(Apply(inner, Change(7, EvfiltUser, EvAdd)) == 0); + Require(Apply(inner, Change(8, EvfiltUser, EvAdd)) == 0); + Require(Apply(outer, Change(watched, EvfiltRead, EvAdd | EvClear)) == 0); + Require(Poll(outer, events, 4) == 0); + + Require(Apply(inner, Change(7, EvfiltUser, 0, NoteTrigger)) == 0); + Require(Poll(outer, events, 4) == 1 && events[0].ident == watched && events[0].data == 1 && (events[0].flags & EvClear) != 0); + Require(Poll(outer, events, 4) == 0); + Require(Poll(inner, events, 4) == 1); + Require(Poll(outer, events, 4) == 0); + + Require(Apply(inner, Change(8, EvfiltUser, 0, NoteTrigger)) == 0); + Require(Poll(outer, events, 4) == 1 && events[0].data == 2); + Require(Poll(outer, events, 4) == 0); + + Require(Apply(outer, Change(watched, EvfiltRead, EvDelete)) == 0); + Require(Apply(outer, Change(watched, EvfiltRead, EvAdd | EvClear)) == 0); + Require(Poll(outer, events, 4) == 1 && events[0].data == 2); + Require(Poll(outer, events, 4) == 0); + Require(close_nid_postfix(outer) == 0 && close_nid_postfix(inner) == 0); +} + static void LowWaterMarks() { const int kq = kqueue_nid_postfix(); int pair[2]; @@ -1497,6 +1525,7 @@ int main() { ReportedKnotesMoveToTail(); Errors(); LowWaterMarks(); + ClearedKqueue(); FileWatch(); #ifndef _WIN32 RenameAndDeleteWatchedFile(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 4968f46ca4..67e7e73a8a 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -249,7 +249,7 @@ - [poll on a stream socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - on Linux a socket that reports `POLLHUP` reports it without `POLLOUT`, as FreeBSD 11.4's `pollscan` clears `POLLOUT` whenever it reports `POLLHUP`; Linux, like FreeBSD, reports `POLLHUP` only once both of the socket's directions are shut. Windows keeps the flags `WSAPoll` reports. Not measured on a console - [kevent EV_CLEAR on a socket or a pipe](../../../core/libs/prx/libkernel/Equeue/src/EdgeWatch.cpp) (libkernel) - FreeBSD 11 activates an `EV_CLEAR` knote when `kqueue_register` finds it ready, which every `EV_ADD` or `EV_ENABLE` of an enabled knote does again, and on each `KNOTE` the socket or pipe raises (`sorwakeup`, `sowwakeup`, `pipeselwakeup`); [`kqueue_scan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c) checks the filter again before reporting, drops a knote that is no longer ready and deactivates a reported one until the next activation. On Linux each such knote registers the host socket or pipe with its own epoll instance in edge-triggered mode (`EPOLLIN | EPOLLRDHUP` for `EVFILT_READ`, `EPOLLOUT` for `EVFILT_WRITE`), and a wakeup of the host descriptor with that mask stands for a `KNOTE`: the knote is then checked with the same readiness test as a level-triggered one, `NOTE_LOWAT` mark included, and a wait blocks on the epoll descriptor instead of the socket, so a socket that stays readable does not wake it. The host's wakeups are not FreeBSD's: FreeBSD raises `sowwakeup` whenever send buffer space is freed, while Linux TCP wakes `EPOLLOUT` waiters only once the socket is writable again after a write found it full (`SOCK_NOSPACE`, [`tcp_check_space`](https://github.com/torvalds/linux/blob/v6.8/net/ipv4/tcp_input.c#L5639)), so a TCP writer that never fills its buffer sees fewer `EVFILT_WRITE` events than on FreeBSD; a host stack may also coalesce data wakeups, so the number of times a busy socket fires can differ. Windows and macOS throw, as the host there has no edge-triggered wait over these descriptors that is wired up; `EV_CLEAR` on the timer, user and vnode filters does not use this and works everywhere. - [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - host pipes are one-way, where FreeBSD's [`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422) opens both ends for reading and writing, so `EVFILT_READ` on the write end and `EVFILT_WRITE` on the read end throw; on macOS `EVFILT_WRITE` on a pipe throws, as the host has no call for a pipe's capacity. `EVFILT_WRITE` reports the free space of the host buffer, not of FreeBSD's 16 KiB one, and on Linux fires when the host says the pipe is writable rather than at `PIPE_BUF` (512) free bytes. Windows pipes cannot be waited on, so a kqueue waiting on one checks it every 1 ms. Otherwise [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1828) and `filt_pipewrite` are followed: `EV_EOF` once the other end closes, `EPIPE` for a write event added after the read end closed, and `NOTE_LOWAT` ignored. On Windows `fcntl` cannot make a pipe write end non-blocking, so the full-pipe and drain case of `EVFILT_WRITE` is tested on Linux only and the `NtQueryInformationFile` write quota is checked there only for the free space, `EV_EOF` and `EPIPE`. -- [kevent on a kqueue](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on a kqueue counts the watched queue's enabled notes that are ready when it is evaluated, where [`filt_kqueue`](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/sys/kern/kern_event.c#L363-L370) reports `kq_count`, the queued notes: a queued note counts until that kqueue is scanned, even once disabled or no longer ready (a socket or pipe read empty, a user note cleared, a watched kqueue drained), so FreeBSD can report it readable where this reports nothing. A queued `EV_ONESHOT` note is reported there with its last count and deleted; here it is evaluated again and, if the watched queue is empty by then, neither reported nor deleted. `EV_CLEAR` throws, as FreeBSD re-arms it only when the watched kqueue queues a note. A cycle of watches throws; a note on the queue itself is `EINVAL`. Not measured on a console +- [kevent on a kqueue](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on a kqueue counts the watched queue's enabled notes that are ready when it is evaluated, where [`filt_kqueue`](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/sys/kern/kern_event.c#L363-L370) reports `kq_count`, the queued notes: a queued note counts until that kqueue is scanned, even once disabled or no longer ready (a socket or pipe read empty, a user note cleared, a watched kqueue drained), so FreeBSD can report it readable where this reports nothing. A queued `EV_ONESHOT` note is reported there with its last count and deleted; here it is evaluated again and, if the watched queue is empty by then, neither reported nor deleted. `EV_CLEAR` fires again only after the watched queue activates a note; one undone before the watching queue polls goes unseen. A cycle of watches throws; a note on the queue itself is `EINVAL`. Not measured on a console - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - a guest socket has the SOL_SOCKET level, eight `IPPROTO_TCP` options, `IPV6_V6ONLY` and ten `IPPROTO_IP` options. `SO_ACCEPTFILTER` (0x1000) fails with `EINVAL` for a set and a get on a socket that is not listening, as a payload process on a PS5 (firmware 13.60) did and as FreeBSD 11.4's `do_setopt_accept_filter` and `do_getopt_accept_filter` do; on a listening socket both throw, as accept filters are not modelled; the other options of these levels fail with `ENOPROTOOPT` and were not all measured. `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and a boolean one reads back as its own bit, as there. Winsock does not clear a pending error when `SO_ERROR` reads it, so on Windows a pending error is reported once and again only after a new `connect`; a second failure with the same code without a `connect` in between is hidden - [TCP_NOPUSH, TCP_NOOPT, TCP option 0x80, IP_RECVDSTADDR, IP_RECVIF, IP_RECVTTL](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept and read back as a payload process on a PS5 (firmware 13.60) did: the TCP ones as the bits 0x1000, 0x8 and 0x10000 (so 0x80 is not FreeBSD's `TCP_KEEPINIT` there), the IP ones as 0 or 1. None of them changes what the socket does: `TCP_NOPUSH` does not hold back sends, and the IP ones add no control message to `recvmsg`. `IP_RECVTTL` was only read, never set, on the console, and an accepted socket takes `TCP_NOPUSH` from its listener, as FreeBSD 11.4's `syncache_socket` does and as measured on that console From 91dea27b2627d9e32d85263dd948a6d2fb382751 Mon Sep 17 00:00:00 2001 From: Omar Elmahy Date: Wed, 28 Oct 2026 12:44:58 +0000 Subject: [PATCH 093/160] test(libSceAudio3d): cover mixer zero-frame, negative pan clamping, and 7-channel downmix --- core/libs/tests/GuestAudio3dMixer.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/core/libs/tests/GuestAudio3dMixer.cpp b/core/libs/tests/GuestAudio3dMixer.cpp index 3e48e963fd..7d09aab14c 100644 --- a/core/libs/tests/GuestAudio3dMixer.cpp +++ b/core/libs/tests/GuestAudio3dMixer.cpp @@ -135,5 +135,25 @@ int main() { Require(half_gain_out.size() == 2 && half_gain_out[0] == 5000 && half_gain_out[1] == 10000, "half gain scales stereo samples by half"); + Audio3dInternal::Source s_zero{pcm, 1, 2, false, 1.0f, 0.0f}; + std::vector zero_out; + Audio3dInternal::MixFrame(&s_zero, 0, 0, zero_out); + Require(zero_out.empty(), "zero frame returns empty output"); + Audio3dInternal::MixFrame(&s_zero, 1, 0, zero_out); + Require(zero_out.empty(), "zero frame with source returns empty output"); + + Audio3dInternal::Source neg_out_of_range{panned, 1, 2, false, 1.0f, -2.0f}; + std::vector neg_out_of_range_out; + Audio3dInternal::MixFrame(&neg_out_of_range, 1, 1, neg_out_of_range_out); + Require(neg_out_of_range_out.size() == 2 && neg_out_of_range_out[0] == 10000 && neg_out_of_range_out[1] == 0, + "negative out of range pan clamps"); + + std::int16_t surround7[7] = {1000, 2000, 3000, 0, 4000, 5000, 6000}; + Audio3dInternal::Source surround7_source{surround7, 1, 7, false, 1.0f, 0.0f}; + std::vector surround7_out; + Audio3dInternal::MixFrame(&surround7_source, 1, 1, surround7_out); + Require(surround7_out.size() == 2 && surround7_out[0] == 10192 && surround7_out[1] == 7657, + "7 channel downmix folds center and surround channels"); + return 0; } From 5f3eab642515b3d59b2f4c2e991b75108ef2fe9c Mon Sep 17 00:00:00 2001 From: NaitikJoshiPro <190247830+NaitikJoshiPro@users.noreply.github.com> Date: Thu, 22 Oct 2026 09:01:08 +0530 Subject: [PATCH 094/160] fix(agc): CLEAR_STATE leaves SPI_PS_IN_CONTROL at NUM_INTERP 2 The GFX10 clear state sets SPI_PS_IN_CONTROL to 0x2 (amdgpu clearstate_gfx10.h). radeonsi relies on it: si_set_tracked_regs_to_clear_state records 0x2 after a CLEAR_STATE, so the register is not written for a pixel shader with two interpolants. The driver cleared it to 0, and such a pixel shader was recompiled with no inputs. CLEAR_STATE and the push-clear CONTEXT_STATE now leave NUM_INTERP 2. The starting context keeps 0, since registration-time decoding reads it and prepares pixel programs only when NUM_INTERP is 0. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Execution/include/QueueState.hpp | 11 ++++++++++- core/libs/prx/libSceAgcDriver/tests/Pm4.cpp | 4 +++- core/libs/prx/libSceAgcDriver/tests/Submit.cpp | 5 +++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp index 44e6d64646..8734c6cda9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp @@ -269,6 +269,15 @@ struct SpareRegisters { SpareRegisters& operator=(SpareRegisters&&) noexcept = default; }; +inline Registers ClearStateContextRegisters() { + static const Registers& cleared = *new Registers([] { + auto registers = BuildInitialContextRegisters(); + registers[0x1b6] = 2; + return registers; + }()); + return cleared; +} + struct Predication { std::uint64_t address = 0; std::uint32_t operation = 0; @@ -293,7 +302,7 @@ struct QueueState { Predication predication; void ClearContext() { - context = InitialContextRegisterSet(); + context = ClearStateContextRegisters(); } void PushContext() { diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 352b5f5086..cefce14d5a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -202,6 +202,7 @@ void testRegisterFile() { void testContextAndBases() { AgcDriver::QueueState state; + check(state.context.at(0x1b6) == 0, "the starting context changed SPI_PS_IN_CONTROL"); check(state.context.contains(0x280) && state.context.at(0x280) == 0x00080008u && state.context.contains(0x281) && state.context.at(0x281) == 0xffff0000u, "initial point context differs from the SDK defaults"); execute(state, makePacket(0x69, {0x280, 0x00180018u, 0x00400008u})); check(state.context.at(0x280) == 0x00180018u && state.context.at(0x281) == 0x00400008u, "explicit point size and limits were not stored"); @@ -209,10 +210,11 @@ void testContextAndBases() { execute(state, makePacket(0x12, {0})); for (const auto offset : {0x010u, 0x011u, 0x10bu, 0x10cu, 0x10du}) check(state.context.at(offset) == 0, "CLEAR_STATE did not restore depth and stencil defaults"); check(state.context.at(0x280) == 0x00080008u && state.context.at(0x281) == 0xffff0000u, "CLEAR_STATE did not restore point size and limits"); + check(state.context.at(0x1b6) == 2u, "CLEAR_STATE did not leave SPI_PS_IN_CONTROL at its clear-state NUM_INTERP of 2"); execute(state, makePacket(0x69, {0x10, 17})); execute(state, makePacket(0x76, {0x20c, 2})); execute(state, makePacket(0x10, {3, 0}, 0x68)); - check(state.context == AgcDriver::InitialContextRegisters() && state.shader.at(0x20c) == 2, "push-clear reset wrong state"); + check(state.context == AgcDriver::ClearStateContextRegisters() && state.context.at(0x1b6) == 2u && state.shader.at(0x20c) == 2, "push-clear reset wrong state"); expectFailure([&] { execute(state, makePacket(0x10, {1, 0}, 0x68)); }, "already pushed"); execute(state, makePacket(0x69, {0x10, 19})); execute(state, makePacket(0x69, {0x280, 0x00100010u, 0x00800010u})); diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index 8b6296af50..aa7d0b6d6a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -131,12 +131,13 @@ void testClearState() { const auto shader = graphics.shader; const auto userConfig = graphics.userConfig; graphics.ClearContext(); - check(graphics.context == AgcDriver::InitialContextRegisters(), "CLEAR_STATE retained context registers"); + check(graphics.context == AgcDriver::ClearStateContextRegisters(), "CLEAR_STATE retained context registers"); check(graphics.shader == shader && graphics.userConfig == userConfig, "CLEAR_STATE reset unrelated registers"); check(graphics.context.count(0x1b3) == 1 && graphics.context.at(0x1b3) == 0 && graphics.context.count(0x1b4) == 1 && graphics.context.at(0x1b4) == 0, "CLEAR_STATE left SPI_PS_INPUT_ENA/ADDR unset"); + check(graphics.context.at(0x1b6) == 2u, "CLEAR_STATE did not leave SPI_PS_IN_CONTROL at its clear-state NUM_INTERP of 2"); graphics.context.emplace(0x10, 31); graphics.ClearContext(); - check(graphics.context == AgcDriver::InitialContextRegisters(), "repeated CLEAR_STATE retained context registers"); + check(graphics.context == AgcDriver::ClearStateContextRegisters(), "repeated CLEAR_STATE retained context registers"); std::array words{0xc0001200, 0, 0}; Packet packet{words.data(), 2, 0, {}}; From 8c2599562ea68b4bc51ad7f0ab5953b36b704810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 24 Oct 2026 00:59:39 +0200 Subject: [PATCH 095/160] docs(agc): record the CLEAR_STATE NUM_INTERP value in debt --- docs/dev/debt/libSceAgcDriver.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 8de8cae97c..97a2a76483 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -69,6 +69,7 @@ - [Registered shader types](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - types 0, 1, 2, 4, 6 and 7 are prepared and type 5 is skipped. Any other type, such as type 8, is logged (`unsupported registered shader type 8; it is not prepared at registration`) and left unprepared at registration, and throws when it is used later. Hello Neighbor 2 (PPSA07426), Ad Infinitum (PPSA10600) and Ghostrunner (PPSA03685) register type 8 headers and run on past them; what a type 8 header describes is unknown, #1892 found no context registers in it. - [NGG vertex workgroups](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - type-2 PRIMGEN programs using DS instructions or `s_barrier` are prepared with the draw's routing and primitive registers on the existing mesh path. The workgroup requirement follows [Mesa's NGG lowering](https://github.com/mihawk-99/PS5_Mesa/blob/b3588f78dd7fd46fbadc7ecd487e5f14d4213e4b/src/amd/common/nir/ac_nir_lower_ngg.c#L1135); the mesh path supplies LDS and two guest lanes per host invocation for wave64 on a 32-lane host subgroup. Console routing has not been measured. Passthrough and tessellation keep their existing paths; unsupported mesh assembly and subgroup budgets throw. - [Shader header copies](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a header bound for a draw or dispatch at another address than the registered one is accepted when its fixed fields match the registered header, and every array it points at still holds the registered bytes; the registered header's metadata is used. `user_data` is not compared (PPSA21567 binds such a copy of a vertex shader header with `user_data` set to null; whether the console reads the copy's `user_data` is unknown). A copy whose registers, context registers, specials or semantics differ, or whose arrays are unreadable, throws `... refers to a replaced shader header`; what the console does with a header a title patches while relocating it is not measured. PPSA01325 (#2914) resolves the ABI with such a copy +- [ClearStateContextRegisters](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) (libSceAgcDriver) - `CLEAR_STATE` and a push-clear `CONTEXT_STATE` (operation 3) reset `SPI_PS_IN_CONTROL` (0x1b6) to NUM_INTERP 2, the value of Linux's GFX10 clear-state table (`clearstate_gfx10.h`, `gfx10_SECT_CONTEXT_def_2`) that radeonsi tracks after `CLEAR_STATE`. The value on a PS5 console is not measured. The CP queue reset (PM4 operation 0x09) and the starting context keep 0, because registration-time decoding prepares a pixel program only while NUM_INTERP is 0. The other registers radeonsi assumes after a clear (`CB_TARGET_MASK`, `CB_SHADER_MASK`, `PA_SU_SC_MODE_CNTL`, `PA_SU_VTX_CNTL`) still reset to the starting table. - [HtileFillCovers](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - a fill is taken to cover a depth surface's whole HTILE when it spans 32 KiB per 1024x512 pixel block from DB_HTILE_DATA_BASE, the pipe-aligned SW_64KB_Z_X metadata extent AMD PAL's gfx10 addrlib gives for GB_ADDR_CONFIG 0x00100044 (1-8 samples, D16/D32); other metadata layouts are not decoded. A partial fill is ignored (the surface keeps its contents); not measured on the console. - [Depth fast clear through HTILE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - a surface cleared by an HTILE fill is cleared to DB_DEPTH_CLEAR, also when it is next sampled. For a depth-only HTILE the clear word also carries a value (radv writes `minz = maxz = round(depth * 0x3fff)`, `HTILE_Z_CLEAR_REG` in Mesa's `src/amd/common/ac_descriptors.h`, and allows TC-compatible fast clears only to 0.0 or 1.0); which of the two the texture unit returns for a cleared tile was not measured. - [Depth-only and stencil HTILE fill](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - in a whole-HTILE fill, ZMask (bits 0-3) 0 clears the depth and SMem (bits 8-9) 0 clears the stencil, while SMem 3 leaves the stencil alone (`HtileFillClears`); other SMem values are taken to leave it alone too. The bit meanings follow the gfx10 HTILE layout as the code reads it, not a measurement; `agc_driver_depth_fast_clear_submit` pins them for the paths the Driver takes. From ec708dbde2c69c929380d6f71b2b6eb6d3570c76 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 22 Oct 2026 09:24:09 +0100 Subject: [PATCH 096/160] fix(libkernel): take the watched kqueue's count and activations in one snapshot A cleared kqueue watch read the watched queue's triggered count and its activation counter under two separate locks, so an activation between the reads was marked seen without making the watch active, and the wakeup was lost. SnapshotTriggered now returns both under one lock. Every poll of an active watch also rechecks the count, as kqueue_scan reruns filt_kqueue: it drops when the watched queue empties and reports the current count. EV_ADD and EV_ENABLE of an existing watch re-arm it. A separate guest_kqueue_cleared_race test triggers the watched queue from another thread while the watching queue waits. --- core/libs/prx/libkernel/CMakeLists.txt | 4 +- core/libs/prx/libkernel/Equeue/Equeue.cpp | 10 ++- core/libs/prx/libkernel/Equeue/Equeue.hpp | 6 +- core/libs/prx/libkernel/Equeue/src/Kqueue.cpp | 28 +++++---- core/libs/tests/GuestKqueue.cpp | 63 ++++++++++++++++++- 5 files changed, 94 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 2a70b082f2..dc1aea96cc 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1752,9 +1752,11 @@ if(BUILD_TESTING) configure_windows_unwind(guest_kqueue_tests) add_test(NAME guest_kqueue COMMAND guest_kqueue_tests) set_tests_properties(guest_kqueue PROPERTIES TIMEOUT 20) + add_test(NAME guest_kqueue_cleared_race COMMAND guest_kqueue_tests cleared-race) + set_tests_properties(guest_kqueue_cleared_race PROPERTIES TIMEOUT 60) if(WIN32) target_link_libraries(guest_kqueue_tests PRIVATE advapi32) - set_tests_properties(guest_kqueue PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_kqueue guest_kqueue_cleared_race PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_socket_descriptors_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketDescriptors.cpp) diff --git a/core/libs/prx/libkernel/Equeue/Equeue.cpp b/core/libs/prx/libkernel/Equeue/Equeue.cpp index 207774faa4..17299485ca 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.cpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.cpp @@ -420,9 +420,15 @@ std::size_t KernelEqueuePrivate::CountTriggeredEvents() { return static_cast(std::count_if(m_events.begin(), m_events.end(), [](const KernelEqueueEvent& event) { return event.enabled && event.triggered; })); } -std::uint64_t KernelEqueuePrivate::Activations() { +KernelEqueuePrivate::TriggeredSnapshot KernelEqueuePrivate::SnapshotTriggered() { std::unique_lock lock(m_mutex); - return m_activations; + if (m_closed) { + return {0, m_activations}; + } + TriggerExpiredTimers(MonotonicNs()); + PollEvents(); + const auto count = static_cast(std::count_if(m_events.begin(), m_events.end(), [](const KernelEqueueEvent& event) { return event.enabled && event.triggered; })); + return {count, m_activations}; } std::vector> KernelEqueuePrivate::WatchedQueues() { diff --git a/core/libs/prx/libkernel/Equeue/Equeue.hpp b/core/libs/prx/libkernel/Equeue/Equeue.hpp index 6c924922b1..0be3819f2e 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.hpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.hpp @@ -92,7 +92,11 @@ public: int GetTriggeredEvents(KernelEvent* ev, int num); bool HasTriggeredEvents(); std::size_t CountTriggeredEvents(); - std::uint64_t Activations(); + struct TriggeredSnapshot { + std::size_t count = 0; + std::uint64_t activations = 0; + }; + TriggeredSnapshot SnapshotTriggered(); std::vector> WatchedQueues(); int WaitForEvents(KernelEvent* ev, int num, uint32_t micros); int WaitForEventsUntil(KernelEvent* ev, int num, std::uint64_t deadlineNanos); diff --git a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp index 2907358440..803268a6c3 100644 --- a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp +++ b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp @@ -235,14 +235,15 @@ struct ClearedKqueueNote { bool PollClearedKqueue(KernelEqueueEvent* e) { auto& note = *static_cast(e->filter.data); - const auto pending = e->filter.watched->CountTriggeredEvents(); - const auto activations = e->filter.watched->Activations(); - if (activations != note.seen) { - note.seen = activations; - if (pending != 0) { - note.active = true; - e->event.data = static_cast(pending); - } + const auto snapshot = e->filter.watched->SnapshotTriggered(); + if (snapshot.activations != note.seen) { + note.seen = snapshot.activations; + note.active = true; + } + if (snapshot.count == 0) { + note.active = false; + } else if (note.active) { + e->event.data = static_cast(snapshot.count); } return note.active; } @@ -285,10 +286,10 @@ int AddKqueueKnote(const KernelEqueueRef& queue, const KernelEqueueRef& watched, event.filter.watched = watched; if ((flags & EV_CLEAR) != 0) { auto note = std::make_shared(); - const auto pending = watched->CountTriggeredEvents(); - note->seen = watched->Activations(); - note->active = pending != 0; - event.event.data = static_cast(pending); + const auto snapshot = watched->SnapshotTriggered(); + note->seen = snapshot.activations; + note->active = snapshot.count != 0; + event.event.data = static_cast(snapshot.count); event.filter.data = note.get(); event.filter.owner = std::move(note); event.filter.pollFunc = PollClearedKqueue; @@ -458,6 +459,9 @@ int ModifyKnote(const KernelEqueueRef& queue, KernelEvent& change) { if (event.enabled && event.filter.pollFunc == PollEdge) { static_cast(event.filter.data)->armed = true; } + if (event.enabled && event.filter.pollFunc == PollClearedKqueue) { + static_cast(event.filter.data)->active = true; + } if (change.filter == EVFILT_USER) { TouchUserKnote(event, change); } else if (change.filter == EVFILT_VNODE) { diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index 55644153b3..f6384b029c 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -611,6 +611,62 @@ static void ClearedKqueue() { Require(close_nid_postfix(outer) == 0 && close_nid_postfix(inner) == 0); } +static void ClearedKqueueRechecksTheWatchedCount() { + const int inner = kqueue_nid_postfix(); + const int outer = kqueue_nid_postfix(); + Require(inner >= 0 && outer >= 0); + const auto watched = static_cast(inner); + KernelEvent events[4]{}; + Require(Apply(inner, Change(7, EvfiltUser, EvAdd | EvClear)) == 0); + Require(Apply(inner, Change(8, EvfiltUser, EvAdd | EvClear)) == 0); + Require(Apply(outer, Change(1, EvfiltUser, EvAdd | EvClear)) == 0); + Require(Apply(outer, Change(watched, EvfiltRead, EvAdd | EvClear)) == 0); + + Require(Apply(inner, Change(7, EvfiltUser, 0, NoteTrigger)) == 0); + Require(Apply(outer, Change(1, EvfiltUser, 0, NoteTrigger)) == 0); + Require(Poll(outer, events, 1) == 1 && events[0].ident == 1 && events[0].filter == EvfiltUser); + Require(Poll(inner, events, 4) == 1 && events[0].ident == 7); + Require(Poll(outer, events, 4) == 0); + + Require(Apply(inner, Change(7, EvfiltUser, 0, NoteTrigger)) == 0); + Require(Apply(inner, Change(8, EvfiltUser, 0, NoteTrigger)) == 0); + Require(Apply(outer, Change(1, EvfiltUser, 0, NoteTrigger)) == 0); + Require(Poll(outer, events, 1) == 1 && events[0].ident == 1); + Require(Poll(inner, events, 1) == 1); + Require(Poll(outer, events, 4) == 1 && events[0].ident == watched && events[0].data == 1); + Require(Poll(outer, events, 4) == 0); + + Require(Apply(outer, Change(watched, EvfiltRead, EvAdd | EvClear)) == 0); + Require(Poll(outer, events, 4) == 1 && events[0].ident == watched && events[0].data == 1); + Require(Poll(outer, events, 4) == 0); + Require(Apply(outer, Change(watched, EvfiltRead, EvDisable)) == 0); + Require(Apply(outer, Change(watched, EvfiltRead, EvEnable)) == 0); + Require(Poll(outer, events, 4) == 1 && events[0].ident == watched && events[0].data == 1); + Require(Poll(inner, events, 4) == 1); + Require(Apply(outer, Change(watched, EvfiltRead, EvAdd | EvClear)) == 0); + Require(Poll(outer, events, 4) == 0); + Require(close_nid_postfix(outer) == 0 && close_nid_postfix(inner) == 0); +} + +static void ClearedKqueueKeepsConcurrentActivations() { + const int inner = kqueue_nid_postfix(); + const int outer = kqueue_nid_postfix(); + Require(inner >= 0 && outer >= 0); + const auto watched = static_cast(inner); + KernelEvent events[4]{}; + Require(Apply(inner, Change(7, EvfiltUser, EvAdd | EvClear)) == 0); + for (std::uintptr_t filler = 100; filler < 20100; ++filler) Require(Apply(inner, Change(filler, EvfiltUser, EvAdd)) == 0); + Require(Apply(outer, Change(watched, EvfiltRead, EvAdd | EvClear)) == 0); + for (int round = 0; round < 5000; ++round) { + std::thread trigger([inner] { Require(Apply(inner, Change(7, EvfiltUser, 0, NoteTrigger)) == 0); }); + const int reported = Wait(outer, events, 4); + trigger.join(); + Require(reported == 1 && events[0].ident == watched); + Require(Poll(inner, events, 4) == 1); + } + Require(close_nid_postfix(outer) == 0 && close_nid_postfix(inner) == 0); +} + static void LowWaterMarks() { const int kq = kqueue_nid_postfix(); int pair[2]; @@ -1498,7 +1554,11 @@ static void StandardInput() { Require(close_nid_postfix(kq) == 0); } -int main() { +int main(int argc, char** argv) { + if (argc == 2 && std::string(argv[1]) == "cleared-race") { + ClearedKqueueKeepsConcurrentActivations(); + return 0; + } StandardInputReplacement(); StandardInput(); CreateAndClose(); @@ -1526,6 +1586,7 @@ int main() { Errors(); LowWaterMarks(); ClearedKqueue(); + ClearedKqueueRechecksTheWatchedCount(); FileWatch(); #ifndef _WIN32 RenameAndDeleteWatchedFile(); From b91db60dba943f1e5925e7d151bf43bf6f8a0dc9 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 22 Oct 2026 10:29:52 +0100 Subject: [PATCH 097/160] test(libkernel): run the cleared kqueue race test on one CPU on Linux On a multi-core Linux machine the race test passed with the split reads it is meant to catch, and caught them only when the process ran on one CPU. It now pins itself to its first allowed CPU on Linux. Windows already catches the split reads unpinned, and pinning there hides them, so it stays unpinned. --- core/libs/tests/GuestKqueue.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index f6384b029c..3bdba2a116 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -16,6 +16,8 @@ #ifdef _WIN32 #include #include +#elif defined(__linux__) +#include #endif #include #ifdef _WIN32 @@ -648,7 +650,23 @@ static void ClearedKqueueRechecksTheWatchedCount() { Require(close_nid_postfix(outer) == 0 && close_nid_postfix(inner) == 0); } +static void RunOnOneCpuOnLinux() { +#ifdef __linux__ + cpu_set_t allowed; + CPU_ZERO(&allowed); + Require(sched_getaffinity(0, sizeof(allowed), &allowed) == 0); + int first = 0; + while (first < CPU_SETSIZE && !CPU_ISSET(first, &allowed)) ++first; + Require(first < CPU_SETSIZE); + cpu_set_t one; + CPU_ZERO(&one); + CPU_SET(first, &one); + Require(sched_setaffinity(0, sizeof(one), &one) == 0); +#endif +} + static void ClearedKqueueKeepsConcurrentActivations() { + RunOnOneCpuOnLinux(); const int inner = kqueue_nid_postfix(); const int outer = kqueue_nid_postfix(); Require(inner >= 0 && outer >= 0); From ab45baa0ab7bd8864bc02528bac21c94d3f9bb7b Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:49:15 +0200 Subject: [PATCH 098/160] test(libSceGameLiveStreaming): cover lifecycle order and null arguments of offline calls --- core/libs/tests/GuestGameLiveStreaming.cpp | 165 +++++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/core/libs/tests/GuestGameLiveStreaming.cpp b/core/libs/tests/GuestGameLiveStreaming.cpp index 7d641c4e23..cb973dfdac 100644 --- a/core/libs/tests/GuestGameLiveStreaming.cpp +++ b/core/libs/tests/GuestGameLiveStreaming.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include static_assert(sizeof(GameLiveStreamingStatus2) == 72); @@ -26,6 +27,163 @@ bool EnableThrowsNotInitialized(bool isEnableLiveStreaming) { return false; } +constexpr int kInvalidArgument = static_cast(0x80A00002); + +GameLiveStreamingFeedbackMessage* AsMessages(std::array& buffer) { + return reinterpret_cast(buffer.data()); +} + +void CheckLifecycleFirst() { + constexpr int notInitialized = static_cast(0x80A00004); + GameLiveStreamingSpoilerTag tag{}; + GameLiveStreamingPresetCommand preset{}; + GameLiveStreamingCameraFrameSetting camera{}; + std::array messages{}; + std::uint32_t count = 0x55; + std::size_t acquired = 0x55; + Require(sceGameLiveStreamingPermitLiveStreaming(true) == notInitialized); + Require(sceGameLiveStreamingPermitServerSideRecording(false) == notInitialized); + Require(sceGameLiveStreamingSetMaxBitrate(6000) == notInitialized); + Require(sceGameLiveStreamingEnableSocialFeedback(true) == notInitialized); + Require(sceGameLiveStreamingSetMetadata(nullptr) == notInitialized); + Require(sceGameLiveStreamingSetMetadata("metadata") == notInitialized); + Require(sceGameLiveStreamingSetSpoilerTag(0, nullptr) == notInitialized); + Require(sceGameLiveStreamingSetSpoilerTag(1, &tag) == notInitialized); + Require(sceGameLiveStreamingClearSpoilerTag() == notInitialized); + Require(sceGameLiveStreamingSetPresetSocialFeedbackCommands(0, nullptr) == notInitialized); + Require(sceGameLiveStreamingSetPresetSocialFeedbackCommands(1, &preset) == notInitialized); + Require(sceGameLiveStreamingClearPresetSocialFeedbackCommands() == notInitialized); + Require(sceGameLiveStreamingSetPresetSocialFeedbackCommandsDescription(nullptr) == notInitialized); + Require(sceGameLiveStreamingSetCameraFrameSetting(nullptr) == notInitialized); + Require(sceGameLiveStreamingSetCameraFrameSetting(&camera) == notInitialized); + Require(sceGameLiveStreamingSetInvitationSessionId(nullptr) == notInitialized); + Require(sceGameLiveStreamingGetSocialFeedbackMessagesCount(0, nullptr) == notInitialized); + Require(sceGameLiveStreamingGetSocialFeedbackMessagesCount(1, &count) == notInitialized); + Require(count == 0x55); + Require(sceGameLiveStreamingGetSocialFeedbackMessages(0, 1, nullptr, nullptr) == notInitialized); + Require(sceGameLiveStreamingGetSocialFeedbackMessages(1, 1, AsMessages(messages), &acquired) == notInitialized); + Require(acquired == 0x55); + Require(sceGameLiveStreamingClearSocialFeedbackMessages(0) == notInitialized); + Require(sceGameLiveStreamingClearSocialFeedbackMessages(1) == notInitialized); + Require(sceGameLiveStreamingPostSocialMessage(nullptr) == notInitialized); + Require(sceGameLiveStreamingPostSocialMessage("message") == notInitialized); +} + +void CheckString(int (APS5_VABI *call)(const char*), std::size_t limit) { + Require(call(nullptr) == kInvalidArgument); + Require(call("") == 0); + Require(call(std::string(limit, 'a').c_str()) == 0); + Require(call(std::string(limit + 1, 'a').c_str()) == kInvalidArgument); + Require(call(std::string(limit + 100, 'a').c_str()) == kInvalidArgument); +} + +void CheckStrings() { + CheckString(sceGameLiveStreamingSetMetadata, 240); + CheckString(sceGameLiveStreamingSetPresetSocialFeedbackCommandsDescription, 720); + CheckString(sceGameLiveStreamingSetInvitationSessionId, 45); + CheckString(sceGameLiveStreamingPostSocialMessage, 360); +} + +void CheckFeedbackTypes() { + std::array messages{}; + for (const int type : {-1, 0, 4, 5, 0x7fffffff}) { + std::uint32_t count = 0xdeadbeef; + Require(sceGameLiveStreamingGetSocialFeedbackMessagesCount(type, &count) == kInvalidArgument); + Require(count == 0xdeadbeef); + std::size_t acquired = 0xdeadbeef; + Require(sceGameLiveStreamingGetSocialFeedbackMessages(type, 1, AsMessages(messages), &acquired) == kInvalidArgument); + Require(acquired == 0xdeadbeef); + Require(sceGameLiveStreamingClearSocialFeedbackMessages(type) == kInvalidArgument); + } + for (const int type : {1, 2, 3}) { + std::uint32_t count = 0xdeadbeef; + Require(sceGameLiveStreamingGetSocialFeedbackMessagesCount(type, &count) == 0); + Require(count == 0); + messages.fill(0xaa); + std::size_t acquired = 0xdeadbeef; + Require(sceGameLiveStreamingGetSocialFeedbackMessages(type, 1, AsMessages(messages), &acquired) == 0); + Require(acquired == 0); + for (const std::uint8_t byte : messages) Require(byte == 0xaa); + acquired = 0xdeadbeef; + Require(sceGameLiveStreamingGetSocialFeedbackMessages(type, 0, AsMessages(messages), &acquired) == 0); + Require(acquired == 0); + Require(sceGameLiveStreamingClearSocialFeedbackMessages(type) == 0); + } + std::size_t acquired = 0xdeadbeef; + Require(sceGameLiveStreamingGetSocialFeedbackMessages(1, 0, nullptr, &acquired) == kInvalidArgument); + Require(sceGameLiveStreamingGetSocialFeedbackMessages(1, 1, nullptr, &acquired) == kInvalidArgument); + Require(sceGameLiveStreamingGetSocialFeedbackMessages(1, 1, AsMessages(messages), nullptr) == kInvalidArgument); + Require(acquired == 0xdeadbeef); + Require(sceGameLiveStreamingGetSocialFeedbackMessagesCount(1, nullptr) == kInvalidArgument); +} + +void CheckArrays() { + GameLiveStreamingSpoilerTag tags[17]{}; + Require(sceGameLiveStreamingSetSpoilerTag(0, nullptr) == kInvalidArgument); + Require(sceGameLiveStreamingSetSpoilerTag(1, nullptr) == kInvalidArgument); + Require(sceGameLiveStreamingSetSpoilerTag(0, tags) == 0); + Require(sceGameLiveStreamingSetSpoilerTag(1, tags) == 0); + Require(sceGameLiveStreamingSetSpoilerTag(16, tags) == 0); + Require(sceGameLiveStreamingSetSpoilerTag(17, tags) == kInvalidArgument); + Require(sceGameLiveStreamingSetSpoilerTag(0x1000, tags) == kInvalidArgument); + for (const std::size_t index : {0u, 15u}) { + for (const std::size_t byte : {0u, 15u, 30u}) { + tags[index].reserved[byte] = 1; + Require(sceGameLiveStreamingSetSpoilerTag(16, tags) == kInvalidArgument); + Require(sceGameLiveStreamingSetSpoilerTag(index, tags) == 0); + tags[index].reserved[byte] = 0; + } + } + tags[16].reserved[0] = 1; + Require(sceGameLiveStreamingSetSpoilerTag(16, tags) == 0); + tags[0].text[192] = 'x'; + Require(sceGameLiveStreamingSetSpoilerTag(1, tags) == 0); + Require(sceGameLiveStreamingClearSpoilerTag() == 0); + Require(sceGameLiveStreamingClearSpoilerTag() == 0); + + GameLiveStreamingPresetCommand presets[6]{}; + Require(sceGameLiveStreamingSetPresetSocialFeedbackCommands(0, nullptr) == kInvalidArgument); + Require(sceGameLiveStreamingSetPresetSocialFeedbackCommands(1, nullptr) == kInvalidArgument); + Require(sceGameLiveStreamingSetPresetSocialFeedbackCommands(0, presets) == 0); + Require(sceGameLiveStreamingSetPresetSocialFeedbackCommands(5, presets) == 0); + Require(sceGameLiveStreamingSetPresetSocialFeedbackCommands(6, presets) == kInvalidArgument); + for (const std::size_t index : {0u, 4u}) { + for (const std::size_t byte : {0u, 30u}) { + presets[index].reserved[byte] = 1; + Require(sceGameLiveStreamingSetPresetSocialFeedbackCommands(5, presets) == kInvalidArgument); + Require(sceGameLiveStreamingSetPresetSocialFeedbackCommands(index, presets) == 0); + presets[index].reserved[byte] = 0; + } + } + presets[5].reserved[0] = 1; + Require(sceGameLiveStreamingSetPresetSocialFeedbackCommands(5, presets) == 0); + presets[0].body[196] = 1; + Require(sceGameLiveStreamingSetPresetSocialFeedbackCommands(5, presets) == 0); + Require(sceGameLiveStreamingClearPresetSocialFeedbackCommands() == 0); + Require(sceGameLiveStreamingClearPresetSocialFeedbackCommands() == 0); + + GameLiveStreamingCameraFrameSetting camera{}; + Require(sceGameLiveStreamingSetCameraFrameSetting(nullptr) == kInvalidArgument); + Require(sceGameLiveStreamingSetCameraFrameSetting(&camera) == 0); + camera.head[8] = 1; + Require(sceGameLiveStreamingSetCameraFrameSetting(&camera) == 0); + camera.head[8] = 0; + for (const std::size_t byte : {0u, 15u, 30u}) { + camera.reserved[byte] = 1; + Require(sceGameLiveStreamingSetCameraFrameSetting(&camera) == kInvalidArgument); + camera.reserved[byte] = 0; + } +} + +void CheckScalars() { + for (const bool value : {true, false}) { + Require(sceGameLiveStreamingPermitLiveStreaming(value) == 0); + Require(sceGameLiveStreamingPermitServerSideRecording(value) == 0); + Require(sceGameLiveStreamingEnableSocialFeedback(value) == 0); + } + for (const std::uint32_t bitrate : {0u, 6000u, 0xffffffffu}) Require(sceGameLiveStreamingSetMaxBitrate(bitrate) == 0); +} + } int main() { @@ -35,6 +193,7 @@ int main() { Require(sceGameLiveStreamingTerminate() == notInitialized); Require(sceGameLiveStreamingGetCurrentStatus2(nullptr) == notInitialized); Require(EnableThrowsNotInitialized(true)); + CheckLifecycleFirst(); for (const std::size_t size : {0u, 1u, 0x3fffu, 0x4001u, 0x100000u}) Require(sceGameLiveStreamingInitialize(size) == invalidArgument); Require(sceGameLiveStreamingInitialize(0x4000) == 0); Require(sceGameLiveStreamingEnableLiveStreaming(true) == 0); @@ -66,8 +225,14 @@ int main() { Require(programInfo[i] == (i < 0x134C ? 0 : 0x7f)); } + CheckScalars(); + CheckStrings(); + CheckFeedbackTypes(); + CheckArrays(); + Require(sceGameLiveStreamingEnableLiveStreaming(false) == 0); Require(sceGameLiveStreamingTerminate() == 0); + CheckLifecycleFirst(); Require(EnableThrowsNotInitialized(false)); std::memset(&status, 0x7f, sizeof(status)); Require(sceGameLiveStreamingGetCurrentStatus2(&status) == notInitialized); From bd19a7e5612cd7988d7e33247951f861c1a46968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= Date: Wed, 28 Oct 2026 13:45:33 +0100 Subject: [PATCH 099/160] docs(libSceGameLiveStreaming): shorten silent stub entry --- docs/dev/debt/libSceGameLiveStreaming.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceGameLiveStreaming.md b/docs/dev/debt/libSceGameLiveStreaming.md index b0aacd6a98..c025ffb571 100644 --- a/docs/dev/debt/libSceGameLiveStreaming.md +++ b/docs/dev/debt/libSceGameLiveStreaming.md @@ -9,7 +9,7 @@ ### Silent stubs -- [sceGameLiveStreamingPermitLiveStreaming](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) - this and fifteen related setting, feedback and message calls (PermitServerSideRecording, SetMaxBitrate, EnableSocialFeedback, SetMetadata, Set/ClearSpoilerTag, Set/ClearPresetSocialFeedbackCommands, ...Description, SetCameraFrameSetting, SetInvitationSessionId, Get/ClearSocialFeedbackMessages, GetSocialFeedbackMessagesCount, PostSocialMessage) validate their arguments locally and store nothing, as no broadcast is ever on air. Order: not initialized (`0x80A00004`), then `0x80A00002` for a null pointer (including a null array with count 0 for SetSpoilerTag and a null `messages` with capacity 0), a feedback type outside 1-3, a string longer than 240 (SetMetadata), 720 (preset description), 45 (invitation session id) or 360 (PostSocialMessage) characters, more than 16 spoiler tags or 5 preset commands, and a non-zero reserved tail (31 bytes at offset 193 of the 224-byte spoiler tag, at offset 197 of the 228-byte preset command, at offset 9 of the camera setting). These checks and layouts come from a report on this pull request that ran each function of the firmware 13.42 `libSceGameLiveStreaming.sprx` under an emulator with the service call stubbed; the order between the argument checks, the camera setting size and the preset and message field layouts beyond the reserved tails are not measured. What the service returns after validation is not measured on a PS5: the calls return 0 and queries report zero messages. +- [sceGameLiveStreamingPermitLiveStreaming](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) - this and fifteen related setting, feedback and message calls (see `Api.hpp`) validate their arguments and store nothing, as no broadcast is ever on air. The checks (not initialized, null pointers, feedback type 1-3, string lengths 240/720/45/360, at most 16 spoiler tags and 5 preset commands, zero reserved tails in the 224-byte spoiler tag, 228-byte preset command and camera setting) come from a report on #9524 that ran each function of the firmware 13.42 module with the service call stubbed. The order between argument checks, the camera setting size and the other struct fields are not measured, nor what the service returns after validation: the calls return 0 and queries report zero messages. ### Unknown function info From 7cdb7a8780fb81e40e0e7e77c398d23f0a4d693d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= Date: Wed, 28 Oct 2026 13:45:37 +0100 Subject: [PATCH 100/160] test(libkernel): shrink the cleared kqueue race, match watched notes by ident, correct the kqueue debt entry Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 2 +- core/libs/tests/GuestKqueue.cpp | 14 +++++++++----- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index dc1aea96cc..c778cf3c74 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1753,7 +1753,7 @@ if(BUILD_TESTING) add_test(NAME guest_kqueue COMMAND guest_kqueue_tests) set_tests_properties(guest_kqueue PROPERTIES TIMEOUT 20) add_test(NAME guest_kqueue_cleared_race COMMAND guest_kqueue_tests cleared-race) - set_tests_properties(guest_kqueue_cleared_race PROPERTIES TIMEOUT 60) + set_tests_properties(guest_kqueue_cleared_race PROPERTIES TIMEOUT 30) if(WIN32) target_link_libraries(guest_kqueue_tests PRIVATE advapi32) set_tests_properties(guest_kqueue guest_kqueue_cleared_race PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index 3bdba2a116..c421f32f45 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -626,16 +626,20 @@ static void ClearedKqueueRechecksTheWatchedCount() { Require(Apply(inner, Change(7, EvfiltUser, 0, NoteTrigger)) == 0); Require(Apply(outer, Change(1, EvfiltUser, 0, NoteTrigger)) == 0); - Require(Poll(outer, events, 1) == 1 && events[0].ident == 1 && events[0].filter == EvfiltUser); Require(Poll(inner, events, 4) == 1 && events[0].ident == 7); + Require(Poll(outer, events, 4) == 1 && events[0].ident == 1 && events[0].filter == EvfiltUser); Require(Poll(outer, events, 4) == 0); Require(Apply(inner, Change(7, EvfiltUser, 0, NoteTrigger)) == 0); Require(Apply(inner, Change(8, EvfiltUser, 0, NoteTrigger)) == 0); Require(Apply(outer, Change(1, EvfiltUser, 0, NoteTrigger)) == 0); - Require(Poll(outer, events, 1) == 1 && events[0].ident == 1); Require(Poll(inner, events, 1) == 1); - Require(Poll(outer, events, 4) == 1 && events[0].ident == watched && events[0].data == 1); + Require(Poll(outer, events, 4) == 2); + const bool watchedFirst = events[0].ident == watched; + const KernelEvent& userEvent = events[watchedFirst ? 1 : 0]; + const KernelEvent& watchedEvent = events[watchedFirst ? 0 : 1]; + Require(userEvent.ident == 1 && userEvent.filter == EvfiltUser); + Require(watchedEvent.ident == watched && watchedEvent.filter == EvfiltRead && watchedEvent.data == 1); Require(Poll(outer, events, 4) == 0); Require(Apply(outer, Change(watched, EvfiltRead, EvAdd | EvClear)) == 0); @@ -673,9 +677,9 @@ static void ClearedKqueueKeepsConcurrentActivations() { const auto watched = static_cast(inner); KernelEvent events[4]{}; Require(Apply(inner, Change(7, EvfiltUser, EvAdd | EvClear)) == 0); - for (std::uintptr_t filler = 100; filler < 20100; ++filler) Require(Apply(inner, Change(filler, EvfiltUser, EvAdd)) == 0); + for (std::uintptr_t filler = 100; filler < 12100; ++filler) Require(Apply(inner, Change(filler, EvfiltUser, EvAdd)) == 0); Require(Apply(outer, Change(watched, EvfiltRead, EvAdd | EvClear)) == 0); - for (int round = 0; round < 5000; ++round) { + for (int round = 0; round < 1250; ++round) { std::thread trigger([inner] { Require(Apply(inner, Change(7, EvfiltUser, 0, NoteTrigger)) == 0); }); const int reported = Wait(outer, events, 4); trigger.join(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 67e7e73a8a..b9097a1f2c 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -249,7 +249,7 @@ - [poll on a stream socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - on Linux a socket that reports `POLLHUP` reports it without `POLLOUT`, as FreeBSD 11.4's `pollscan` clears `POLLOUT` whenever it reports `POLLHUP`; Linux, like FreeBSD, reports `POLLHUP` only once both of the socket's directions are shut. Windows keeps the flags `WSAPoll` reports. Not measured on a console - [kevent EV_CLEAR on a socket or a pipe](../../../core/libs/prx/libkernel/Equeue/src/EdgeWatch.cpp) (libkernel) - FreeBSD 11 activates an `EV_CLEAR` knote when `kqueue_register` finds it ready, which every `EV_ADD` or `EV_ENABLE` of an enabled knote does again, and on each `KNOTE` the socket or pipe raises (`sorwakeup`, `sowwakeup`, `pipeselwakeup`); [`kqueue_scan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c) checks the filter again before reporting, drops a knote that is no longer ready and deactivates a reported one until the next activation. On Linux each such knote registers the host socket or pipe with its own epoll instance in edge-triggered mode (`EPOLLIN | EPOLLRDHUP` for `EVFILT_READ`, `EPOLLOUT` for `EVFILT_WRITE`), and a wakeup of the host descriptor with that mask stands for a `KNOTE`: the knote is then checked with the same readiness test as a level-triggered one, `NOTE_LOWAT` mark included, and a wait blocks on the epoll descriptor instead of the socket, so a socket that stays readable does not wake it. The host's wakeups are not FreeBSD's: FreeBSD raises `sowwakeup` whenever send buffer space is freed, while Linux TCP wakes `EPOLLOUT` waiters only once the socket is writable again after a write found it full (`SOCK_NOSPACE`, [`tcp_check_space`](https://github.com/torvalds/linux/blob/v6.8/net/ipv4/tcp_input.c#L5639)), so a TCP writer that never fills its buffer sees fewer `EVFILT_WRITE` events than on FreeBSD; a host stack may also coalesce data wakeups, so the number of times a busy socket fires can differ. Windows and macOS throw, as the host there has no edge-triggered wait over these descriptors that is wired up; `EV_CLEAR` on the timer, user and vnode filters does not use this and works everywhere. - [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - host pipes are one-way, where FreeBSD's [`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422) opens both ends for reading and writing, so `EVFILT_READ` on the write end and `EVFILT_WRITE` on the read end throw; on macOS `EVFILT_WRITE` on a pipe throws, as the host has no call for a pipe's capacity. `EVFILT_WRITE` reports the free space of the host buffer, not of FreeBSD's 16 KiB one, and on Linux fires when the host says the pipe is writable rather than at `PIPE_BUF` (512) free bytes. Windows pipes cannot be waited on, so a kqueue waiting on one checks it every 1 ms. Otherwise [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1828) and `filt_pipewrite` are followed: `EV_EOF` once the other end closes, `EPIPE` for a write event added after the read end closed, and `NOTE_LOWAT` ignored. On Windows `fcntl` cannot make a pipe write end non-blocking, so the full-pipe and drain case of `EVFILT_WRITE` is tested on Linux only and the `NtQueryInformationFile` write quota is checked there only for the free space, `EV_EOF` and `EPIPE`. -- [kevent on a kqueue](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on a kqueue counts the watched queue's enabled notes that are ready when it is evaluated, where [`filt_kqueue`](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/sys/kern/kern_event.c#L363-L370) reports `kq_count`, the queued notes: a queued note counts until that kqueue is scanned, even once disabled or no longer ready (a socket or pipe read empty, a user note cleared, a watched kqueue drained), so FreeBSD can report it readable where this reports nothing. A queued `EV_ONESHOT` note is reported there with its last count and deleted; here it is evaluated again and, if the watched queue is empty by then, neither reported nor deleted. `EV_CLEAR` fires again only after the watched queue activates a note; one undone before the watching queue polls goes unseen. A cycle of watches throws; a note on the queue itself is `EINVAL`. Not measured on a console +- [kevent on a kqueue](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on a kqueue counts the watched queue's enabled notes that are ready when it is evaluated, where [`filt_kqueue`](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/sys/kern/kern_event.c#L363-L370) reports `kq_count`, the queued notes: a queued note counts until that kqueue is scanned, even once disabled or no longer ready (a socket or pipe read empty, a user note cleared, a watched kqueue drained), so FreeBSD can report it readable where this reports nothing. A queued `EV_ONESHOT` note is reported there with its last count and deleted; here it is evaluated again and, if the watched queue is empty by then, neither reported nor deleted. `EV_CLEAR` fires again only after the watched queue activates a note; one undone before the watching queue polls is missed. A cycle of watches throws; a note on the queue itself is `EINVAL`. Not measured on a console - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - a guest socket has the SOL_SOCKET level, eight `IPPROTO_TCP` options, `IPV6_V6ONLY` and ten `IPPROTO_IP` options. `SO_ACCEPTFILTER` (0x1000) fails with `EINVAL` for a set and a get on a socket that is not listening, as a payload process on a PS5 (firmware 13.60) did and as FreeBSD 11.4's `do_setopt_accept_filter` and `do_getopt_accept_filter` do; on a listening socket both throw, as accept filters are not modelled; the other options of these levels fail with `ENOPROTOOPT` and were not all measured. `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and a boolean one reads back as its own bit, as there. Winsock does not clear a pending error when `SO_ERROR` reads it, so on Windows a pending error is reported once and again only after a new `connect`; a second failure with the same code without a `connect` in between is hidden - [TCP_NOPUSH, TCP_NOOPT, TCP option 0x80, IP_RECVDSTADDR, IP_RECVIF, IP_RECVTTL](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept and read back as a payload process on a PS5 (firmware 13.60) did: the TCP ones as the bits 0x1000, 0x8 and 0x10000 (so 0x80 is not FreeBSD's `TCP_KEEPINIT` there), the IP ones as 0 or 1. None of them changes what the socket does: `TCP_NOPUSH` does not hold back sends, and the IP ones add no control message to `recvmsg`. `IP_RECVTTL` was only read, never set, on the console, and an accepted socket takes `TCP_NOPUSH` from its listener, as FreeBSD 11.4's `syncache_socket` does and as measured on that console From 638996829b1a6c9b3800d7aac89a8409af6cbe49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= Date: Wed, 28 Oct 2026 13:45:47 +0100 Subject: [PATCH 101/160] docs(libkernel): shorten the kevent on a kqueue debt entry Co-Authored-By: Claude Sonnet 5.5 --- 3rdparty/SPIRV-Headers | 2 +- 3rdparty/SPIRV-Tools | 2 +- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/3rdparty/SPIRV-Headers b/3rdparty/SPIRV-Headers index b824a462d4..01e0577914 160000 --- a/3rdparty/SPIRV-Headers +++ b/3rdparty/SPIRV-Headers @@ -1 +1 @@ -Subproject commit b824a462d4256d720bebb40e78b9eb8f78bbb305 +Subproject commit 01e0577914a75a2569c846778c2f93aa8e6feddd diff --git a/3rdparty/SPIRV-Tools b/3rdparty/SPIRV-Tools index 262bdab481..7f2d9ee926 160000 --- a/3rdparty/SPIRV-Tools +++ b/3rdparty/SPIRV-Tools @@ -1 +1 @@ -Subproject commit 262bdab48146c937467f826699a40da0fdfc0f1a +Subproject commit 7f2d9ee926f98fc77a3ed1e1e0f113b8c9c49458 diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index b9097a1f2c..dd97a1905f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -249,7 +249,7 @@ - [poll on a stream socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - on Linux a socket that reports `POLLHUP` reports it without `POLLOUT`, as FreeBSD 11.4's `pollscan` clears `POLLOUT` whenever it reports `POLLHUP`; Linux, like FreeBSD, reports `POLLHUP` only once both of the socket's directions are shut. Windows keeps the flags `WSAPoll` reports. Not measured on a console - [kevent EV_CLEAR on a socket or a pipe](../../../core/libs/prx/libkernel/Equeue/src/EdgeWatch.cpp) (libkernel) - FreeBSD 11 activates an `EV_CLEAR` knote when `kqueue_register` finds it ready, which every `EV_ADD` or `EV_ENABLE` of an enabled knote does again, and on each `KNOTE` the socket or pipe raises (`sorwakeup`, `sowwakeup`, `pipeselwakeup`); [`kqueue_scan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c) checks the filter again before reporting, drops a knote that is no longer ready and deactivates a reported one until the next activation. On Linux each such knote registers the host socket or pipe with its own epoll instance in edge-triggered mode (`EPOLLIN | EPOLLRDHUP` for `EVFILT_READ`, `EPOLLOUT` for `EVFILT_WRITE`), and a wakeup of the host descriptor with that mask stands for a `KNOTE`: the knote is then checked with the same readiness test as a level-triggered one, `NOTE_LOWAT` mark included, and a wait blocks on the epoll descriptor instead of the socket, so a socket that stays readable does not wake it. The host's wakeups are not FreeBSD's: FreeBSD raises `sowwakeup` whenever send buffer space is freed, while Linux TCP wakes `EPOLLOUT` waiters only once the socket is writable again after a write found it full (`SOCK_NOSPACE`, [`tcp_check_space`](https://github.com/torvalds/linux/blob/v6.8/net/ipv4/tcp_input.c#L5639)), so a TCP writer that never fills its buffer sees fewer `EVFILT_WRITE` events than on FreeBSD; a host stack may also coalesce data wakeups, so the number of times a busy socket fires can differ. Windows and macOS throw, as the host there has no edge-triggered wait over these descriptors that is wired up; `EV_CLEAR` on the timer, user and vnode filters does not use this and works everywhere. - [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - host pipes are one-way, where FreeBSD's [`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422) opens both ends for reading and writing, so `EVFILT_READ` on the write end and `EVFILT_WRITE` on the read end throw; on macOS `EVFILT_WRITE` on a pipe throws, as the host has no call for a pipe's capacity. `EVFILT_WRITE` reports the free space of the host buffer, not of FreeBSD's 16 KiB one, and on Linux fires when the host says the pipe is writable rather than at `PIPE_BUF` (512) free bytes. Windows pipes cannot be waited on, so a kqueue waiting on one checks it every 1 ms. Otherwise [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1828) and `filt_pipewrite` are followed: `EV_EOF` once the other end closes, `EPIPE` for a write event added after the read end closed, and `NOTE_LOWAT` ignored. On Windows `fcntl` cannot make a pipe write end non-blocking, so the full-pipe and drain case of `EVFILT_WRITE` is tested on Linux only and the `NtQueryInformationFile` write quota is checked there only for the free space, `EV_EOF` and `EPIPE`. -- [kevent on a kqueue](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on a kqueue counts the watched queue's enabled notes that are ready when it is evaluated, where [`filt_kqueue`](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/sys/kern/kern_event.c#L363-L370) reports `kq_count`, the queued notes: a queued note counts until that kqueue is scanned, even once disabled or no longer ready (a socket or pipe read empty, a user note cleared, a watched kqueue drained), so FreeBSD can report it readable where this reports nothing. A queued `EV_ONESHOT` note is reported there with its last count and deleted; here it is evaluated again and, if the watched queue is empty by then, neither reported nor deleted. `EV_CLEAR` fires again only after the watched queue activates a note; one undone before the watching queue polls is missed. A cycle of watches throws; a note on the queue itself is `EINVAL`. Not measured on a console +- [kevent on a kqueue](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on a kqueue counts the watched queue's enabled notes that are ready when it is evaluated, where [`filt_kqueue`](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/sys/kern/kern_event.c#L363-L370) reports `kq_count`, the queued notes: a queued note counts until that kqueue is scanned, even once disabled or no longer ready (a socket or pipe read empty, a user note cleared, a watched kqueue drained), so FreeBSD can report it readable where this reports nothing. A queued `EV_ONESHOT` note is reported there with its last count and deleted; here it is evaluated again and, if the watched queue is empty by then, neither reported nor deleted. `EV_CLEAR` fires again only after the watched queue activates a note; one undone before the watching queue polls is missed. A cycle of watches throws; a note on its own queue is `EINVAL`. Not measured on a console - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - a guest socket has the SOL_SOCKET level, eight `IPPROTO_TCP` options, `IPV6_V6ONLY` and ten `IPPROTO_IP` options. `SO_ACCEPTFILTER` (0x1000) fails with `EINVAL` for a set and a get on a socket that is not listening, as a payload process on a PS5 (firmware 13.60) did and as FreeBSD 11.4's `do_setopt_accept_filter` and `do_getopt_accept_filter` do; on a listening socket both throw, as accept filters are not modelled; the other options of these levels fail with `ENOPROTOOPT` and were not all measured. `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and a boolean one reads back as its own bit, as there. Winsock does not clear a pending error when `SO_ERROR` reads it, so on Windows a pending error is reported once and again only after a new `connect`; a second failure with the same code without a `connect` in between is hidden - [TCP_NOPUSH, TCP_NOOPT, TCP option 0x80, IP_RECVDSTADDR, IP_RECVIF, IP_RECVTTL](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept and read back as a payload process on a PS5 (firmware 13.60) did: the TCP ones as the bits 0x1000, 0x8 and 0x10000 (so 0x80 is not FreeBSD's `TCP_KEEPINIT` there), the IP ones as 0 or 1. None of them changes what the socket does: `TCP_NOPUSH` does not hold back sends, and the IP ones add no control message to `recvmsg`. `IP_RECVTTL` was only read, never set, on the console, and an accepted socket takes `TCP_NOPUSH` from its listener, as FreeBSD 11.4's `syncache_socket` does and as measured on that console From 634e7eb41fb02b44068bf664de34cf72a744741c Mon Sep 17 00:00:00 2001 From: PrivacyOnion Date: Wed, 28 Oct 2026 13:05:26 +0100 Subject: [PATCH 102/160] feat(libScePosixForWebKit): implement opendir, closedir, readdir, readdir_r --- core/libs/prx/libScePosixForWebKit/Export.cpp | 15 ++++++++++ .../prx/libScePosixForWebKit/include/Api.hpp | 5 ++++ core/libs/tests/GuestPosixFilesystem.cpp | 30 +++++++++++++++++++ docs/dev/debt/libScePosixForWebKit.md | 1 + 4 files changed, 51 insertions(+) diff --git a/core/libs/prx/libScePosixForWebKit/Export.cpp b/core/libs/prx/libScePosixForWebKit/Export.cpp index d1a3a2320e..4b721881f7 100644 --- a/core/libs/prx/libScePosixForWebKit/Export.cpp +++ b/core/libs/prx/libScePosixForWebKit/Export.cpp @@ -282,4 +282,19 @@ int APS5_VABI pthread_setname_np_nid_postfix(Pthread thread, const char* name) { return 0; } +void* APS5_VABI opendir_nid_disambig1_nid_postfix(const char* path) { + return opendir_nid_postfix(path); +} + +int APS5_VABI closedir_nid_disambig1_nid_postfix(void* handle) { + return closedir_nid_postfix(handle); +} + +GuestDirectoryEntry* APS5_VABI readdir_nid_disambig1_nid_postfix(void* handle) { + return readdir_nid_postfix(handle); +} + +int APS5_VABI readdir_r_nid_disambig1_nid_postfix(void* handle, GuestDirectoryEntry* entry, GuestDirectoryEntry** result) { + return readdir_r_nid_postfix(handle, entry, result); +} } diff --git a/core/libs/prx/libScePosixForWebKit/include/Api.hpp b/core/libs/prx/libScePosixForWebKit/include/Api.hpp index 21562c5fcb..1b07808bca 100644 --- a/core/libs/prx/libScePosixForWebKit/include/Api.hpp +++ b/core/libs/prx/libScePosixForWebKit/include/Api.hpp @@ -1,11 +1,16 @@ #pragma once +#include "prx/libc/include/GuestDirectory.hpp" #include "prx/libScePosixForWebKit/GuestResolver.hpp" #include "prx/libSceLibcInternal/include/Statvfs.hpp" #include "SceTypes.hpp" extern "C" { +void* APS5_VABI opendir_nid_disambig1_nid_postfix(const char* path); +int APS5_VABI closedir_nid_disambig1_nid_postfix(void* handle); +GuestDirectoryEntry* APS5_VABI readdir_nid_disambig1_nid_postfix(void* handle); +int APS5_VABI readdir_r_nid_disambig1_nid_postfix(void* handle, GuestDirectoryEntry* entry, GuestDirectoryEntry** result); int APS5_VABI fnmatch_nid_postfix(const char* pattern, const char* string, int flags); char* APS5_VABI strcasestr_nid_postfix(const char* text, const char* needle); void APS5_VABI arc4random_buf_nid_postfix(void* buffer, std::size_t size); diff --git a/core/libs/tests/GuestPosixFilesystem.cpp b/core/libs/tests/GuestPosixFilesystem.cpp index c18d690982..d0ba05956a 100644 --- a/core/libs/tests/GuestPosixFilesystem.cpp +++ b/core/libs/tests/GuestPosixFilesystem.cpp @@ -173,6 +173,36 @@ int main() { Require(mmap_nid_postfix(nullptr, page, 8, 0x1002, -1, 0) == failed && *__error_nid_postfix() == 22); *__error_nid_postfix() = 0; Require(__wrap_mmap(nullptr, page, 8, 0x1002, -1, 0) == failed && *__error_nid_postfix() == 22); + Require(opendir_nid_disambig1_nid_postfix(nullptr) == nullptr && *__error_nid_postfix() == 14); + Require(opendir_nid_disambig1_nid_postfix("") == nullptr && *__error_nid_postfix() == 2); + Require(opendir_nid_disambig1_nid_postfix("/anyps5-posix-test/missing-directory") == nullptr && *__error_nid_postfix() == 2); + void* const testDirectory = opendir_nid_disambig1_nid_postfix("/anyps5-posix-test"); + Require(testDirectory != nullptr); + bool foundDot = false; + bool foundDotDot = false; + bool foundFixed = false; + while (auto* const entry = readdir_nid_disambig1_nid_postfix(testDirectory)) { + Require(entry->nameLength == std::strlen(entry->name)); + Require(entry->recordLength >= 8 + entry->nameLength); + Require(entry->fileNumber != 0); + if (std::strcmp(entry->name, ".") == 0) foundDot = true; + if (std::strcmp(entry->name, "..") == 0) foundDotDot = true; + if (std::strcmp(entry->name, "fixed") == 0) foundFixed = true; + } + Require(foundDot && foundDotDot && foundFixed); + Require(readdir_nid_disambig1_nid_postfix(testDirectory) == nullptr); + Require(readdir_nid_disambig1_nid_postfix(nullptr) == nullptr && *__error_nid_postfix() == 9); + Require(closedir_nid_disambig1_nid_postfix(testDirectory) == 0); + Require(closedir_nid_disambig1_nid_postfix(nullptr) == -1 && *__error_nid_postfix() == 9); + void* const readdirDirectory = opendir_nid_disambig1_nid_postfix("/anyps5-posix-test"); + Require(readdirDirectory != nullptr); + GuestDirectoryEntry readdirEntry{}; + GuestDirectoryEntry* readdirResult = nullptr; + Require(readdir_r_nid_disambig1_nid_postfix(readdirDirectory, &readdirEntry, &readdirResult) == 0); + Require(readdirResult == &readdirEntry); + Require(readdirEntry.nameLength == std::strlen(readdirEntry.name)); + Require(closedir_nid_disambig1_nid_postfix(readdirDirectory) == 0); + Require(readdir_r_nid_disambig1_nid_postfix(nullptr, &readdirEntry, &readdirResult) == 9); RemovePathAlias_nid_no_patch("/anyps5-posix-test"); Require(std::filesystem::remove_all(root) > 0); } diff --git a/docs/dev/debt/libScePosixForWebKit.md b/docs/dev/debt/libScePosixForWebKit.md index 5b1e4f0a2f..d1e5c9565c 100644 --- a/docs/dev/debt/libScePosixForWebKit.md +++ b/docs/dev/debt/libScePosixForWebKit.md @@ -2,6 +2,7 @@ ### Unknown function info +- [opendir, closedir, readdir, readdir_r](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) - whether the console implementation of directory enumeration functions in libScePosixForWebKit applies WebKit sandboxing, custom path rewriting, or internal directory filtering is unverified; the exports forward directly to libc directory operations (opendir, closedir, readdir, readdir_r). Names and NIDs (ay3uROQAc5A, XepdqehVYe4, lybyyKtP54c, J0kng1yac3M) appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePosixForWebKit.c). - [gethostbyname](../../../core/libs/prx/libScePosixForWebKit/Resolver.cpp) (libScePosixForWebKit) - numeric IPv4 forms, trailing whitespace and 64-bit accumulation follow FreeBSD 11.4 [inet_aton](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/inet/inet_addr.c); clearing the resolver error on numeric success follows [fakeaddr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/net/gethostnamadr.c). Exact PS5 behavior is unverified; hostname lookup still uses the host resolver - [clock_gettime_np, getenv_np, gethostbyname_r (`scePosixForWebKit_gethostbyname_r`), mmap_np, msetname, pthread_setschedparam_np, setenv_np, sysinfo (`scePosixForWebKit_sysinfo`)](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) - names/NIDs appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePosixForWebKit.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); credible prototypes are unknown. These exports use renamed helpers; defining `gethostbyname_r` would interpose on the host function this library calls itself. All are parameterless import stubs, not ABI claims; callers must not use them until signatures are verified. From bcf05215aa99fc7efd88e556459ceb271c39e050 Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Wed, 28 Oct 2026 07:54:04 -0500 Subject: [PATCH 103/160] test(libScePlayGo): cover handle and pointer validation in scePlayGoGetInstallSpeed and scePlayGoGetEta --- core/libs/tests/GuestPlayGo.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/libs/tests/GuestPlayGo.cpp b/core/libs/tests/GuestPlayGo.cpp index 8f4eb7b5ae..55711df9c3 100644 --- a/core/libs/tests/GuestPlayGo.cpp +++ b/core/libs/tests/GuestPlayGo.cpp @@ -167,6 +167,16 @@ static int CheckNoPlayGoFile(const char* root) { Require(scePlayGoGetProgress(handle, &chunk, 1, &progress) == 0 && progress.progress_size == 1 && progress.total_size == 1); Require(scePlayGoSetInstallSpeed(handle + 1, 2) == badHandle); Require(scePlayGoSetInstallSpeed(handle, 2) == 0); + std::int32_t speed = 0; + Require(scePlayGoGetInstallSpeed(handle + 1, &speed) == badHandle); + Require(scePlayGoGetInstallSpeed(handle, nullptr) == badPointer); + Require(scePlayGoGetInstallSpeed(handle, &speed) == 0 && speed == 2); + int64_t eta = -1; + Require(scePlayGoGetEta(handle + 1, &chunk, 1, &eta) == badHandle); + Require(scePlayGoGetEta(handle, &chunk, 1, nullptr) == badPointer); + Require(scePlayGoGetEta(handle, nullptr, 1, &eta) == badPointer); + Require(scePlayGoGetEta(handle, &chunk, 0, &eta) == badSize); + Require(scePlayGoGetEta(handle, &chunk, 1, &eta) == 0 && eta == 0); Require(scePlayGoPrefetchOptionalChunk(handle + 1, 0, nullptr) == badHandle); Require(scePlayGoPrefetchOptionalChunk(handle, 0, nullptr) == 0); Require(scePlayGoClose(handle + 1) == badHandle); From 098f9a8621e7ead17a259e54b178ad8d3d840979 Mon Sep 17 00:00:00 2001 From: PrivacyOnion Date: Wed, 28 Oct 2026 13:05:50 +0100 Subject: [PATCH 104/160] feat(libSceInvitationDialog): implement sceInvitationDialogGetStatus and stub OpenA and GetResultA --- .../prx/libSceInvitationDialog/Export.cpp | 16 ++++++++++++ .../libSceInvitationDialog/include/Api.hpp | 3 +++ core/libs/tests/GuestInvitationDialog.cpp | 26 +++++++++++++++++++ docs/dev/debt/libSceInvitationDialog.md | 2 ++ 4 files changed, 47 insertions(+) diff --git a/core/libs/prx/libSceInvitationDialog/Export.cpp b/core/libs/prx/libSceInvitationDialog/Export.cpp index e198f7e1b0..0b41f35ebe 100644 --- a/core/libs/prx/libSceInvitationDialog/Export.cpp +++ b/core/libs/prx/libSceInvitationDialog/Export.cpp @@ -111,4 +111,20 @@ int APS5_VABI sceInvitationDialogGetResult(InvitationDialogResult* result) { return COMMON_DIALOG_OK; } +int APS5_VABI sceInvitationDialogGetStatus_nid_postfix(void) { + DialogState& dialog = Dialog(); + std::lock_guard lock(dialog.mutex); + return dialog.status; +} + +int APS5_VABI sceInvitationDialogOpenA_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceInvitationDialogGetResultA_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceInvitationDialog/include/Api.hpp b/core/libs/prx/libSceInvitationDialog/include/Api.hpp index 7e53014e52..8a8f8593a9 100644 --- a/core/libs/prx/libSceInvitationDialog/include/Api.hpp +++ b/core/libs/prx/libSceInvitationDialog/include/Api.hpp @@ -54,5 +54,8 @@ int APS5_VABI sceInvitationDialogOpen(const InvitationDialogParam* param); int APS5_VABI sceInvitationDialogTerminate(void); int APS5_VABI sceInvitationDialogUpdateStatus(void); int APS5_VABI sceInvitationDialogClose(void); +int APS5_VABI sceInvitationDialogGetStatus_nid_postfix(void); +int APS5_VABI sceInvitationDialogOpenA_nid_postfix(void); +int APS5_VABI sceInvitationDialogGetResultA_nid_postfix(void); } diff --git a/core/libs/tests/GuestInvitationDialog.cpp b/core/libs/tests/GuestInvitationDialog.cpp index 0552fd1e08..c329c3a17e 100644 --- a/core/libs/tests/GuestInvitationDialog.cpp +++ b/core/libs/tests/GuestInvitationDialog.cpp @@ -142,6 +142,31 @@ void TestCanceled() { Require(sceInvitationDialogTerminate() == 0, "terminate again"); } +template +void RequireNotImplemented(TCall call, const char* message) { + bool threw = false; + try { + call(); + } catch (const std::runtime_error& error) { + threw = std::string(error.what()).find("not implemented") != std::string::npos; + } + Require(threw, message); +} + +void TestStatusAndPending() { + int callbackArg = 0; + Require(sceInvitationDialogGetStatus_nid_postfix() == StatusNone, "get status before initialization"); + RequireNotImplemented([] { sceInvitationDialogOpenA_nid_postfix(); }, "openA is not implemented"); + RequireNotImplemented([] { sceInvitationDialogGetResultA_nid_postfix(); }, "getResultA is not implemented"); + Require(sceInvitationDialogInitialize() == 0, "initialize for status check"); + Require(sceInvitationDialogGetStatus_nid_postfix() == StatusInitialized, "status after initialize"); + const InvitationDialogParam send = MakeParam(ModeSend, &callbackArg); + Require(sceInvitationDialogOpen(&send) == 0, "open for status check"); + Require(sceInvitationDialogGetStatus_nid_postfix() == StatusFinished, "status after open"); + Require(sceInvitationDialogTerminate() == 0, "terminate after status check"); + Require(sceInvitationDialogGetStatus_nid_postfix() == StatusNone, "status after terminate"); +} + } int main() { @@ -149,5 +174,6 @@ int main() { TestCommonDialogPreconditions(); TestOpenChecks(); TestCanceled(); + TestStatusAndPending(); return 0; } diff --git a/docs/dev/debt/libSceInvitationDialog.md b/docs/dev/debt/libSceInvitationDialog.md index 9f2880d0fa..796df23dc8 100644 --- a/docs/dev/debt/libSceInvitationDialog.md +++ b/docs/dev/debt/libSceInvitationDialog.md @@ -6,6 +6,8 @@ ### Unknown function info +- [sceInvitationDialogGetStatus](../../../core/libs/prx/libSceInvitationDialog/Export.cpp) (libSceInvitationDialog, PS5) - the name and NID (`EiF92YDNHRA`) appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceInvitationDialog.c). It returns the current dialog status (`NONE`, `INITIALIZED`, `RUNNING` or `FINISHED`) like `sceInvitationDialogUpdateStatus`; console behaviour is not measured. +- [sceInvitationDialogOpenA, sceInvitationDialogGetResultA](../../../core/libs/prx/libSceInvitationDialog/Export.cpp) (libSceInvitationDialog, PS5) - the names and NIDs (`sAxbHhAWMXM`, `WuuUhuKOxwQ`) appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceInvitationDialog.c); their parameter and result layouts and behaviour are unverified. The declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. - [sceInvitationDialogOpen, sceInvitationDialogGetResult](../../../core/libs/prx/libSceInvitationDialog/include/Api.hpp) (libSceInvitationDialog, PS4) - the field names and the 144-byte `InvitationDialogParam` come from the [shadPS4 header](https://github.com/shadps4-emu/shadPS4/blob/2cdefaa1298c6e4ece6c74bd84e90f3906282d51/src/core/libraries/invitation_dialog/invitation_dialog.h). They match the supplied `libSceInvitationDialog.sprx`: export `8XKR6wa64iQ` at 0xFF0 checks the reserved bytes 0x18..0x37 of the result and writes its status at 0x08, and the helper at 0x410 copies eight bytes of the saved open parameter to 0x00, the eight result bytes to 0x08 and up to 16 20-byte online ids through the pointer at 0x10, after a four-byte count. A param whose `size` is not 144, or whose `mode` is neither 1 (send) nor 2 (receive), throws, and so does a result with a non-zero reserved byte. `baseParam`, `userId` and `dataParam`, with its 1 to 16 recipients, are not checked. - [sceInvitationDialogOpen, sceInvitationDialogClose, sceInvitationDialogGetResult](../../../core/libs/prx/libSceInvitationDialog/Export.cpp) (libSceInvitationDialog, PS4) - two paths throw because shadPS4 and the other common dialogs here return different errors: `Open` before `Initialize` (shadPS4 `INVALID_STATE`, the Signin family `NOT_INITIALIZED`) and `Close` on a dialog that is not running, which is every `Close` after `Initialize` here (shadPS4 `NOT_RUNNING`, the Signin family 0). `GetResult` before the dialog finishes returns `NOT_FINISHED` even for a null result, as in shadPS4. shadPS4 returns the `result` value from `GetResult`; this returns 0, which the module writes at 0x08 as `errorCode`. From 9f6c3527ed5f528f77d3f7061e0fd505e9f19193 Mon Sep 17 00:00:00 2001 From: SeaSunPT Date: Wed, 28 Oct 2026 12:56:22 +0000 Subject: [PATCH 105/160] fix(libSceAudioOut): refuse system debug state and chat calls before sceAudioOut2Initialize #9838 read sceAudioOut2SetSystemDebugState in PS5 firmware 3.20 and 4.03: it returns 0x80268006 (not initialized) before sceAudioOut2Initialize, ahead of its pointer, id and size checks. The debt entry for sceAudioOut2EnableChat and sceAudioOut2DisableChat records the same 0x80268006 before AudioOut2 initialization. Both were left out while AudioOut2 initialization was not tracked; AudioOut2IsInitialized_nid_no_patch now tracks it, so all three return 0x80268006 first and change no state. The two debt entries drop the closed gap. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAudioOut/src/AudioOut2System.cpp | 4 ++++ .../libSceAudioOut/tests/AudioOut2Latency.cpp | 17 +++++++++++++++++ docs/dev/debt/libSceAudioOut.md | 4 ++-- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp index 0a88e02d41..be4675b94c 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp @@ -8,6 +8,7 @@ static constexpr std::uint32_t MAX_SUPPORTED_3D_OUTPUT = 2; static constexpr int SCE_AUDIO_OUT2_ERROR_INVALID_PARAM = static_cast(0x80268001); +static constexpr int SCE_AUDIO_OUT2_ERROR_NOT_INITIALIZED = static_cast(0x80268006); static std::atomic g_chatEnabled{false}; @@ -37,6 +38,7 @@ int APS5_VABI sceAudioOut2SetSystemDebugState(const AudioOut2SystemDebugStatePar constexpr std::uint32_t DebugStateIdUnmodelled = 0x20; constexpr std::size_t LoudnessParamSize = 8; constexpr std::size_t UnmodelledParamSize = 4; + if (!AudioOut2IsInitialized_nid_no_patch()) return SCE_AUDIO_OUT2_ERROR_NOT_INITIALIZED; if (param == nullptr) return ErrorInvalidPointer; if (param->debug_state_id == DebugStateIdUnmodelled && param->param_size == UnmodelledParamSize) NotImplemented_nid_no_patch(__func__); if (param->debug_state_id != DebugStateIdLoudness || param->param_size != LoudnessParamSize) return SCE_AUDIO_OUT2_ERROR_INVALID_PARAM; @@ -46,11 +48,13 @@ int APS5_VABI sceAudioOut2SetSystemDebugState(const AudioOut2SystemDebugStatePar } int APS5_VABI sceAudioOut2EnableChat(void) { + if (!AudioOut2IsInitialized_nid_no_patch()) return SCE_AUDIO_OUT2_ERROR_NOT_INITIALIZED; if (!g_chatEnabled.exchange(true)) AUDIOOUT2_TRACE("t=%.3f EnableChat: chat audio is allowed alongside the title's; the host mixes no chat audio\n", AudioOut2TraceSeconds()); return 0; } int APS5_VABI sceAudioOut2DisableChat(void) { + if (!AudioOut2IsInitialized_nid_no_patch()) return SCE_AUDIO_OUT2_ERROR_NOT_INITIALIZED; if (g_chatEnabled.exchange(false)) AUDIOOUT2_TRACE("t=%.3f DisableChat: chat audio is no longer allowed alongside the title's\n", AudioOut2TraceSeconds()); return 0; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp index 12dbcb3854..17599fad65 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp @@ -85,6 +85,22 @@ void TestMasteringBeforeInitialize() { Require(sceAudioOut2MasteringSetParam(nullptr, 0, 2) == errorNotInitialized, "the AudioOut2 initialization check comes before the parameter checks"); } +void TestSystemBeforeInitialize() { + std::uint32_t loudness[2] = {1, 0}; + const auto set = [](std::uint32_t id, void* data, std::size_t size) { + const AudioOut2SystemDebugStateParam param{id, 0, data, size}; + return sceAudioOut2SetSystemDebugState(¶m); + }; + Require(set(1, loudness, 8) == errorNotInitialized, "a debug state before AudioOut2 initialization is refused"); + Require(sceAudioOut2SetSystemDebugState(nullptr) == errorNotInitialized, "the AudioOut2 initialization check comes before the null debug state check"); + Require(set(2, loudness, 8) == errorNotInitialized, "the AudioOut2 initialization check comes before the debug state id check"); + Require(set(1, loudness, 4) == errorNotInitialized, "the AudioOut2 initialization check comes before the debug state size check"); + Require(set(0x20, loudness, 4) == errorNotInitialized, "debug state id 0x20 of size 4 before AudioOut2 initialization is refused"); + Require(set(1, nullptr, 8) == errorNotInitialized, "a loudness debug state without a param before AudioOut2 initialization is refused"); + Require(sceAudioOut2EnableChat() == errorNotInitialized, "enabling chat before AudioOut2 initialization is refused"); + Require(sceAudioOut2DisableChat() == errorNotInitialized, "disabling chat before AudioOut2 initialization is refused"); +} + void TestMasteringInit() { const AudioOut2MasteringParamsHeader params{1u}; AudioOut2MasteringStates storage{}; @@ -246,6 +262,7 @@ void TestLowLatencyService() { int main() { TestMasteringBeforeInitialize(); + TestSystemBeforeInitialize(); Require(sceAudioOut2Initialize() == 0, "initialization must succeed"); Require(sceAudioOut2Initialize() == static_cast(0x80268004), "a second AudioOut2 initialization fails"); Require(sceAudioOutInit() == 0, "the first legacy initialization succeeds after AudioOut2's"); diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 78403e7c26..46aabde459 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -33,7 +33,7 @@ - [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - the two 32-bit arguments and selector validation match the system library: selector 0 accepts outputs 0 through 2; selector 255 accepts outputs 1 and 2. No third latency argument exists. The host has no system 3D rendering stage, so the call still does not apply latency and does not reproduce the system initialization and output-availability checks. - [sceAudioOut2MasteringInit and sceAudioOut2MasteringInitEx](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - the initialized flag and the flag checks follow the PS5 13.60 module (0x22360): a repeated initialization returns 0, other flags return `0x80268201`, and InitEx also accepts bit 31, which the console passes to the audio server as a mastering mode. Nothing is sent to an audio server. PPSA01289 calls Init twice before Term twice; by the module its second Term gets `0x80268203`, and the title only logs a non-zero result - [sceAudioOut2MasteringGetState and sceAudioOut2MasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - the state layout, the states ids 1 (default) and 2 (V2), the descriptor ids `0x01010001`, `0x01020001` and `0x01010003` and the error `0x80268204` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp), not a console measurement. GetState zeroes the states and writes the two descriptors; with no mastering stage the compressor and limiter values stay zero. SetParam accepts any parameter set and records nothing, as KytyPS5 does. The checks before them follow the PS5 13.60 module: both return `0x80268203` before mastering is initialized, GetState returns `0x8026800C` for a null state, SetParam returns `0x80268201` for a null parameter and `0x80268205` for flags above 1. The console's checks of `output` and `user` are not reproduced; both are ignored -- [sceAudioOut2SetSystemDebugState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - records the debug state id in the trace and returns 0, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp) does; the host has no system mix to apply it to. Id `0x20` with a 4-byte param throws, and the not-initialized error is not returned. Not measured on a console +- [sceAudioOut2SetSystemDebugState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - records the debug state id in the trace and returns 0, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp) does; the host has no system mix to apply it to. Id `0x20` with a 4-byte param throws. Not measured on a console - [sceAudioOut2GetSpeakerArrayCoefficients](../../../core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp) (libSceAudioOut) - validates the handle and the coefficient count like the ambisonics call, then writes the front pair at full gain and silence for every other speaker, ignoring the position, spread, height awareness and downmix spread radius: [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp) and [prosper](https://github.com/mattias800/prosper/blob/main/prosper/src/hle/audio/hle_audio.cpp) both return that pair. The console's VBAP gains over the array's directions are not measured - [sceAudioOutSetMixLevelPadSpk](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - no source applies the level (shadPS4 only stores it); it is assumed to be a linear gain on the port's own output, and the -9 dB default (11626) is applied to every pad speaker port, including those of titles that never call the function - [sceAudioOutGetLastOutputTime](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - signature and behaviour taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [fpPS4](https://github.com/red-prig/fpPS4) reimplementations of the PS4 library, not confirmed on a PS5 title: the time is the process time at which `sceAudioOutOutput` or `sceAudioOutOutputs` accepted the port's last block, not the time the block reached the device @@ -132,7 +132,7 @@ - [sceAudioOutMasteringInit, sceAudioOutMasteringTerm and sceAudioOutMasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - non-zero flags, a null pointer and a params id other than 1 throw; a repeated Init, a Term without Init and a SetParam or GetState before Init are unknown. There is no mastering stage, so Init and Term keep no state and SetParam reads only the id. The signatures and the default params id 1 follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut.cs#L312-L344), not a console measurement. SharpProspero documents flag bit 0 as `SwapEqCompressor`, and shadPS4's PS4 stub [rejects any non-zero Init flags](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/audio/audioout.cpp#L956-L962) with 0x80260201 - [sceAudioOutMasteringGetState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - the states id 1 and the meter offsets 32, 128, 240, 272 and 304 in a 336-byte struct follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Audio/AudioOutMastering.cs#L97-L110), not a console measurement. With no mastering stage it zeroes the compressor input RMS and gain, and the limiter input, output and gain peaks, and leaves the other bytes, including the descriptors the AudioOut2 call writes, untouched; zero is not known to be what the console reports for an idle chain. A null pointer and a states id other than 1 throw; the console error codes are unknown -- [sceAudioOut2EnableChat and sceAudioOut2DisableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) - both PS5 exports take no arguments, return 0x80268006 before AudioOut2 initialization, and invoke the system chat service with selector 3. A negative service result maps to 0x80268000, otherwise they return zero. The host only records a local enabled flag, without initialization validation or a chat service; repeated-call and no-session behavior therefore still depends on the missing service. +- [sceAudioOut2EnableChat and sceAudioOut2DisableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) - both PS5 exports take no arguments, return 0x80268006 before AudioOut2 initialization, and invoke the system chat service with selector 3. A negative service result maps to 0x80268000, otherwise they return zero. The host only records a local enabled flag, without a chat service; repeated-call and no-session behavior therefore still depends on the missing service. - [AudioOut2 contexts](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device and no `sceAudioOut2ContextAdvance` calls read a grain's port buffers at the next push, at least 100 us after the grain was pushed, and play the mix behind a 40 ms cushion that the queue level leaves out. When the hardware reads the buffers is not known. The timing follows a title whose mixer fills a buffer after pushing it, reuses it two pushes later, and may still be writing it when the next push follows within microseconds. The last grain pushed before a title stops pushing is never read or played. - [sceAudioOut2ContextGetQueueLevel](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device counts the grains that `sceAudioOut2ContextAdvance` completed and no push has consumed as queued, together with the pushed grains beyond the 40 ms cushion, up to `queue_depth`. Whether the console counts them is unknown. Pushes that fill the cushion leave the level at 0, so a title that renders as many grains as the level leaves room for and pushes them on another thread rendered a full queue again after each of those pushes: PPSA23226's Wwise sink passes the available count to the sound engine as the number of frames to render, advances once per frame and pushes on a separate thread, and exceeded 16 completed grains. Counted, the completed grains keep such a title at most `queue_depth` grains ahead of its pushes. - [sceAudioOut2LoInit, sceAudioOut2LoContext* and sceAudioOut2LoPort* calls](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp) (libSceAudioOut) have no low-latency path: each forwards to its `sceAudioOut2` counterpart, so grain and queue depth are the context's `num_grains` and `queue_depth`, and playback keeps the 40 ms cushion. [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L489-L545) only says each call takes the same handles and parameter blocks as its counterpart and drives the low-latency clock with a shallow queue and a small grain ([docs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/docs/audio.md#L662-L668)). Unverified: the console's grain and depth limits, its error for a handle of the other service (accepted here) and what `sceAudioOut2LoInit` returns when repeated (0 here). From dbb3e716c02440a2a0b814dacaacb0adae137c70 Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Wed, 28 Oct 2026 07:57:55 -0500 Subject: [PATCH 106/160] test(libSceAudiodec): cover null ctrl and member pointers in sceAudiodecCreateDecoder and sceAudiodecDecode --- core/libs/tests/GuestAudiodec.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/libs/tests/GuestAudiodec.cpp b/core/libs/tests/GuestAudiodec.cpp index 6461dd9bf4..0d5378d6ff 100644 --- a/core/libs/tests/GuestAudiodec.cpp +++ b/core/libs/tests/GuestAudiodec.cpp @@ -197,6 +197,8 @@ constexpr std::int32_t errorArg = static_cast(0x807F0002); constexpr std::int32_t errorInvalidParamSize = static_cast(0x807F0004); constexpr std::int32_t errorInvalidBsiInfoSize = static_cast(0x807F0005); constexpr std::int32_t errorInvalidCtrlPointer = static_cast(0x807F0008); +constexpr std::int32_t errorInvalidParamPointer = static_cast(0x807F0009); +constexpr std::int32_t errorInvalidBsiInfoPointer = static_cast(0x807F000A); constexpr std::int32_t errorInvalidAuInfoPointer = static_cast(0x807F000B); constexpr std::int32_t errorInvalidHandle = static_cast(0x807F000F); constexpr std::int32_t errorInvalidWordLength = static_cast(0x807F0010); @@ -387,6 +389,10 @@ int main() { Require(sceAudiodecCreateDecoder(&ctrl, 2) == errorArg); for (std::uint32_t type = 1; type <= 3; ++type) Require(sceAudiodecInitLibrary(type) == 0); Require(sceAudiodecCreateDecoder(nullptr, 2) == errorInvalidCtrlPointer); + AudiodecCtrl nullParamCtrl{nullptr, &mp3Info, nullptr, nullptr}; + Require(sceAudiodecCreateDecoder(&nullParamCtrl, 2) == errorInvalidParamPointer); + AudiodecCtrl nullBsiCtrl{&mp3, nullptr, nullptr, nullptr}; + Require(sceAudiodecCreateDecoder(&nullBsiCtrl, 2) == errorInvalidBsiInfoPointer); Require(sceAudiodecCreateDecoder(&ctrl, 0) == errorInvalidType); Require(sceAudiodecCreateDecoder(&ctrl, 5) == errorInvalidType); mp3.ui_size = 12; @@ -449,6 +455,9 @@ int main() { const std::int32_t handle = sceAudiodecCreateDecoder(&ctrl, 2); Require(handle > 0); Streams streams; + Require(sceAudiodecDecode(handle, nullptr) == errorInvalidCtrlPointer); + Require(sceAudiodecDecode(handle, &nullParamCtrl) == errorInvalidParamPointer); + Require(sceAudiodecDecode(handle, &nullBsiCtrl) == errorInvalidBsiInfoPointer); Require(sceAudiodecDecode(handle, &ctrl) == errorInvalidAuInfoPointer); Require(Decode(handle, ctrl, streams, MP3_MONO, 0) == errorInvalidAuSize); Require(Decode(handle, ctrl, streams, MP3_MONO, sizeof(MP3_MONO), 0) == errorInvalidPcmSize); From a9b6a5f37c4229054dc4fc1c34cba2fb23e5a1c4 Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Wed, 28 Oct 2026 08:02:24 -0500 Subject: [PATCH 107/160] test(libScePngEnc): cover null pointers and invalid handle in scePngEncCreate and scePngEncEncode --- core/libs/tests/GuestPngEnc.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/libs/tests/GuestPngEnc.cpp b/core/libs/tests/GuestPngEnc.cpp index 53cbcd808f..ffb1e99c81 100644 --- a/core/libs/tests/GuestPngEnc.cpp +++ b/core/libs/tests/GuestPngEnc.cpp @@ -41,7 +41,9 @@ int main() { Require(scePngEncQueryMemorySize(&bad) == invalidSize); std::vector memory((memorySize + 7) / 8); void* handle = nullptr; + Require(scePngEncCreate(nullptr, memory.data(), memorySize, &handle) == invalidAddr); Require(scePngEncCreate(&create, nullptr, memorySize, &handle) == invalidAddr); + Require(scePngEncCreate(&create, memory.data(), memorySize, nullptr) == invalidAddr); Require(scePngEncCreate(&create, memory.data(), memorySize - 1, &handle) == invalidSize); Require(scePngEncCreate(&create, memory.data(), memorySize, &handle) == 0 && reinterpret_cast(handle) % 16 == 0); @@ -134,6 +136,7 @@ int main() { encode.png_mem_size = static_cast(png.size()); PngEncEncodeParam invalid = encode; + Require(scePngEncEncode(nullptr, &encode, nullptr) == invalidHandle); Require(scePngEncEncode(handle, nullptr, nullptr) == invalidAddr); invalid.image_mem_addr = nullptr; Require(scePngEncEncode(handle, &invalid, nullptr) == invalidAddr); @@ -154,6 +157,12 @@ int main() { invalid.image_mem_size = pitch * (height - 1) + width * 4; Require(scePngEncEncode(handle, &invalid, nullptr) == invalidParam); invalid = encode; + invalid.png_mem_addr = nullptr; + Require(scePngEncEncode(handle, &invalid, nullptr) == invalidAddr); + invalid = encode; + invalid.image_mem_size = 0; + Require(scePngEncEncode(handle, &invalid, nullptr) == invalidSize); + invalid = encode; invalid.png_mem_size = 0; Require(scePngEncEncode(handle, &invalid, nullptr) == invalidSize); invalid = encode; From 7106eed0e06b60a693421f2fabde8a5ecf426fba Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Wed, 28 Oct 2026 08:05:09 -0500 Subject: [PATCH 108/160] test(libSceAcm): cover argument validation in sceAcm_FFT and destroyed context in sceAcmBatchStartBuffers --- core/libs/tests/GuestAcm.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/tests/GuestAcm.cpp b/core/libs/tests/GuestAcm.cpp index 80d918bda6..0ef10e0adf 100644 --- a/core/libs/tests/GuestAcm.cpp +++ b/core/libs/tests/GuestAcm.cpp @@ -70,6 +70,13 @@ int main() { Require(descriptor[6] == reinterpret_cast(sources) && descriptor[7] == reinterpret_cast(sources)); for (std::size_t index = 8; index < 16; ++index) Require(descriptor[index] == 0); commandBatch = {commands, 0, sizeof(commands)}; + Require(sceAcm_FFT(&commandBatch, 512, 0, 0, sources, 0, outputs, 0) == BadPointer); + Require(sceAcm_FFT(&commandBatch, 256, 1, 0, sources, 0, outputs, 0) == BadPointer); + Require(sceAcm_FFT(&commandBatch, 4096, 1, 0, sources, 0, outputs, 0) == BadPointer); + Require(sceAcm_FFT(&commandBatch, 512, 1, 2, sources, 0, outputs, 0) == BadPointer); + Require(sceAcm_FFT(&commandBatch, 512, 1, 0, sources, 2, outputs, 0) == BadPointer); + Require(sceAcm_FFT(&commandBatch, 512, 1, 0, nullptr, 0, outputs, 0) == BadPointer); + Require(sceAcm_FFT(&commandBatch, 512, 1, 0, sources, 0, nullptr, 0) == BadPointer); Require(sceAcm_FFT(&commandBatch, 2048, 1, 0, sources, 0, outputs, 0) == BadPointer); Require(sceAcm_FFT(&commandBatch, 512, 1, 0, sources, 0, outputs, 3) == BadPointer); Require(sceAcm_FFT(&commandBatch, 512, 1, 0, sources, 0, outputs, 0) == 0); @@ -111,6 +118,7 @@ int main() { Require(sceAcmContextDestroy(context1) == 0); Require(sceAcmContextDestroy(context1) == BadContext); Require(sceAcmBatchStartBuffer(context1, nullptr, 0, nullptr, &batch1) == BadContext); + Require(sceAcmBatchStartBuffers(context1, 0, nullptr, nullptr, &batch1) == BadContext); Require(sceAcmBatchWait(context1, batch1, 0) == BadContext); Require(sceAcmContextDestroy(context2) == 0); From 1f21b45361fa8cae0a69f2ce549c26058b20c172 Mon Sep 17 00:00:00 2001 From: Kovalev Vladislav Date: Mon, 26 Oct 2026 16:53:27 +0600 Subject: [PATCH 109/160] fix(tools): keep SELF PT_PHDR tables in the load image --- tools/tests/test_unwrap_self.py | 77 +++++++++++++++++++++++++++++++++ tools/unwrap_self.py | 19 +++++++- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index a64f76f9ab..250120681a 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -20,6 +20,7 @@ PT_LOAD = 1 PT_DYNAMIC = 2 PT_TLS = 7 PT_NOTE = 4 +PT_PHDR = 6 SCE_COMMENT = 0x6FFFFF00 SCE_VERSION = 0x6FFFFF01 FIRST_REGION = 0x400 @@ -849,6 +850,82 @@ class UnwrapTest(unittest.TestCase): self.assertEqual(len(headers), count) self.assertTrue(all(header[0] == 0 for header in headers[1:])) + def test_phdr_names_the_rewritten_table_in_the_load_image(self): + table_size = 2 * 56 + for address in (0, 0x4000): + for delta in (0, 0x40): + for recorded in (False, True): + with self.subTest(address=address, delta=delta, recorded=recorded): + table_offset = PAGE + delta + segments = [(PT_PHDR, 4, table_offset, address + delta, table_size, 8), + (PT_LOAD, 4, PAGE, address, 0x400, PAGE)] + table = build_elf(segments, table_offset)[table_offset:table_offset + table_size] + payload = bytearray(b"load payload\0" * 80)[:0x400] + payload[delta:delta + table_size] = table + records = [(0x100800, 0x3000, len(payload))] + if recorded: + records.insert(0, (0x800, 0x3000 + delta, table_size)) + container = build_container(segments, records, {0x3000: payload}, + program_offset=table_offset) + result, output = self.convert(container) + self.assertEqual(result.returncode, 0, result.stderr) + data, headers = read_program_headers(output) + phdr, load = headers + rewritten = struct.unpack_from(" tail[0]): tail = (header["offset"], header["source"] - header["offset"]) continue - if header["vaddr"] != 0: + if header["vaddr"] != 0 or header["type"] == PT_PHDR: host = load_for(header["vaddr"], header["size"]) if host is None: if header["size"] == 0: @@ -266,7 +267,18 @@ def unwrap(path, output, verbose=False): records = read_records(data) inner, elf_header, headers = parse_elf(data) + phdrs = [header for header in headers if header["type"] == PT_PHDR] + if len(phdrs) > 1: + raise Refused("more than one PT_PHDR segment") + phdr = phdrs[0] if phdrs else None + if phdr is not None: + if phdr["size"] < len(headers) * 56 or phdr["memory"] < len(headers) * 56: + raise Refused("PT_PHDR does not cover the program header table") + if any(header["type"] == PT_LOAD and header["index"] < phdr["index"] for header in headers): + raise Refused("PT_PHDR must precede the load segments") place_segments(data, headers, records) + if phdr is not None and phdr["host"] is None: + raise Refused("PT_PHDR is not in a file-backed load segment") check_decrypted(data, headers) regions = [] @@ -350,6 +362,9 @@ def unwrap(path, output, verbose=False): if header["new"] + header["size"] > len(result): raise Refused("segment %d escaped the output" % header["index"]) + if phdr is not None: + program_offset = phdr["new"] + struct.pack_into(" Date: Wed, 28 Oct 2026 14:18:04 +0100 Subject: [PATCH 110/160] feat(libSceNpManager): answer sceNpWaitAsync as sceNpPollAsync does --- core/libs/prx/libSceNpManager/Export.cpp | 7 ++-- core/libs/prx/libSceNpManager/include/Api.hpp | 2 +- core/libs/tests/GuestNpManager.cpp | 33 +++++++++++++++++++ docs/dev/debt/libSceNpManager.md | 2 +- 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index 4d60e0869e..046eafb727 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -887,8 +887,11 @@ int APS5_VABI sceNpPollAsync(int req_id, int* result) { return NP_POLL_ASYNC_FINISHED; } -int APS5_VABI sceNpWaitAsync(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpWaitAsync(int req_id, int* result) { + if (!result) return SCE_NP_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(requestMutex); + if (!liveRequests.count(req_id)) return SCE_NP_ERROR_REQUEST_NOT_FOUND; + *result = SCE_NP_ERROR_SIGNED_OUT; return 0; } diff --git a/core/libs/prx/libSceNpManager/include/Api.hpp b/core/libs/prx/libSceNpManager/include/Api.hpp index 0fd7a18a74..85ea6fc53f 100644 --- a/core/libs/prx/libSceNpManager/include/Api.hpp +++ b/core/libs/prx/libSceNpManager/include/Api.hpp @@ -184,7 +184,7 @@ int APS5_VABI sceNpGetOnlineId(int userId, NpOnlineId* onlineId); int APS5_VABI sceNpGetState(int userId, uint32_t* state); int APS5_VABI sceNpHasSignedUp(int userId, bool* hasSignedUp); int APS5_VABI sceNpPollAsync(int reqId, int* result); -int APS5_VABI sceNpWaitAsync(void); +int APS5_VABI sceNpWaitAsync(int reqId, int* result); void APS5_VABI sceNpRegisterGamePresenceCallback(void* callback, void* userdata); int APS5_VABI sceNpRegisterNpReachabilityStateCallback(void* callback, void* userdata); int APS5_VABI sceNpUnregisterNpReachabilityStateCallback(void); diff --git a/core/libs/tests/GuestNpManager.cpp b/core/libs/tests/GuestNpManager.cpp index 6ccef24325..eaa10034b0 100644 --- a/core/libs/tests/GuestNpManager.cpp +++ b/core/libs/tests/GuestNpManager.cpp @@ -225,6 +225,38 @@ void CheckAsyncRequestLifecycle() { Require(sceNpDeleteRequest(unusedAsyncResult) == RequestNotFound, "second delete"); } +void CheckWaitAsync() { + NpCreateAsyncRequestParameter param{}; + const int liveId = sceNpCreateAsyncRequest(¶m); + Require(liveId > 0, "sceNpCreateAsyncRequest must return a positive request id"); + const int deletedId = sceNpCreateAsyncRequest(¶m); + Require(deletedId > 0 && deletedId != liveId, "sceNpCreateAsyncRequest must return distinct positive request ids"); + Require(sceNpDeleteRequest(deletedId) == 0, "initial delete"); + + constexpr int resultSentinel = 0x5a5a5a5a; + for (const int id : {0, -1, 0x7fffffff, deletedId}) { + int result = resultSentinel; + Require(sceNpWaitAsync(id, &result) == RequestNotFound, "wait must reject an absent or deleted id"); + Require(result == resultSentinel, "wait must not touch the result for an absent or deleted id"); + Require(sceNpWaitAsync(id, nullptr) == InvalidArgument, "wait must reject a null result before the id"); + } + Require(sceNpWaitAsync(liveId, nullptr) == InvalidArgument, "wait must reject a null result for a live request"); + + for (int round = 0; round < 2; ++round) { + int result = resultSentinel; + Require(sceNpWaitAsync(liveId, &result) == 0, "wait on a live async request"); + Require(result == SignedOut, "wait must report the signed-out result on a live async request"); + } + int pollResult = resultSentinel; + Require(sceNpPollAsync(liveId, &pollResult) == 0, "poll after wait"); + Require(pollResult == SignedOut, "poll after wait must report the same result"); + + Require(sceNpDeleteRequest(liveId) == 0, "delete after wait"); + int result = resultSentinel; + Require(sceNpWaitAsync(liveId, &result) == RequestNotFound, "wait after delete"); + Require(result == resultSentinel, "wait must not touch the result after delete"); +} + void CheckIdMapperRequests() { for (const int reqId : {0, -1, -0x7fffffff}) { Require(sceNpIdMapperAbortRequest(reqId) == IdMapperRefusedRequestId, "sceNpIdMapperAbortRequest must refuse a request id of 0 or less"); @@ -558,6 +590,7 @@ int main() { CheckLegacyStateCallback(); CheckRequestTimeouts(); CheckAsyncRequestLifecycle(); + CheckWaitAsync(); CheckIdMapperRequests(); CheckManagerIntRequests(); CheckIdMapperLookup("sceNpIdMapperNpIdToAccountId", sceNpIdMapperNpIdToAccountId); diff --git a/docs/dev/debt/libSceNpManager.md b/docs/dev/debt/libSceNpManager.md index 8afcb90846..39afaf5ca5 100644 --- a/docs/dev/debt/libSceNpManager.md +++ b/docs/dev/debt/libSceNpManager.md @@ -6,7 +6,6 @@ ### Unknown function info -- [sceNpWaitAsync](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `jyi5p9XWUSs`) - the public PS5 3.20 inventory identifies this as a function. Its signature, request completion, output and error behavior are not verified for PS5, so the no-parameter placeholder throws instead of reporting a fabricated completion result. - [libSceNpManager pending exports](../../../core/libs/prx/libSceNpManager/Export.cpp) - 144 named callable exports are present in the local `libSceNpManager.sprx` export table and the public PS5 3.20 symbol list. Their signatures and behavior are unverified; each declaration uses the no-parameter unknown-signature convention and throws through `NotImplemented_nid_no_patch`. - [sceNpGetSandboxInfo](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `ZVZ++68nNI8`) - the name hashes to the imported NID (found with nidhunt, confirmed with `tools/nid_names.py`). STAR WARS: Galactic Racer (PPSA28416) imports it; its OnlineSubsystemPS5 calls it with one pointer to a 0x50-byte stack buffer and reads the buffer as a string (discussions/3086). The returned string, the exact parameter struct and console errors are unknown, so every call throws without reading or writing caller memory. A working implementation belongs with that title's bring-up. @@ -44,4 +43,5 @@ - [sceNpManagerIntAuthGetAuthorizationCode, sceNpManagerIntAuthGetIdToken, sceNpManagerIntGetAuthorizationCode, sceNpManagerIntGetAuthorizationCodeWithPsnoUri](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the arities and checks are [measured on firmware 13.42 `libSceNpManager.sprx` by BunnyFl](https://github.com/boykopovar/AnyPS5/pull/10890#issuecomment-6397162960), with no libSceNpAuth layout: user id -1 or a required pointer null gives `SCE_NP_ERROR_INVALID_ARGUMENT`, as do the eight-argument call's values outside 0 to 3 and 1 to 3; a mode other than 0, 3 or 5 gives `0x80550004`; the URI form refuses a size outside 1 to 0x801 with `0x80550011`, then the mode, then a URI of 0x2000 bytes or more with `0x80550005` (terminator unmeasured). Past the checks the answer follows main's offline model, not a console measurement: `SCE_NP_ERROR_SIGNED_OUT`, writing nothing, for any request id; the request lookup and the URI form's `sceHttpUriParse` step are not modelled - [sceNpManagerIntGetAccountId, sceNpManagerIntGetAccountIdSdk, sceNpManagerIntGetUserIdByAccountId](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - still throw: their answer or their input is an account id, and what a signed-out user's account id is depends on the offline account-id model of [#6990](https://github.com/boykopovar/AnyPS5/pull/6990) and [#7412](https://github.com/boykopovar/AnyPS5/pull/7412), as for `sceNpIdMapperAccountIdToNpId` - [sceNpPollAsync](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - a null result pointer returns `SCE_NP_ERROR_INVALID_ARGUMENT` before the id is read, a request id that was never created or was deleted returns `SCE_NP_ERROR_REQUEST_NOT_FOUND` without writing the caller's result, and a live request keeps its signed-out completion `0` with `SCE_NP_ERROR_SIGNED_OUT`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp) does in `sceNpPollAsync` ([#9277](https://github.com/boykopovar/AnyPS5/issues/9277)). The parts of that model beyond the validation are not implemented: shadPS4 keeps a per-request state machine decorated by `sceNpCheckNpAvailability` and neighbors and refuses `InvalidId` for a ready async request in wait/poll, whose completion publication, poll-versus-wait precedence and already-complete errors remain unattributable to the PS5 +- [sceNpWaitAsync](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - not measured on a PS5: how the console waits on a request still running is unknown. No request is ever running here, so it never blocks and gives `sceNpPollAsync`'s answers, with that entry's gaps, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/45837a89a617ac394c1844ec95a28471fb79ec08/src/core/libraries/np/np_manager.cpp#L402-L452) does with two identical bodies. The `(request id, result)` signature follows shadPS4 and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Np/NpAccount.cs#L138-L146) - [sceNpCreateAsyncRequest](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the parameter is not read ([NpCreateAsyncRequestParameter](../../../core/libs/SceTypes.hpp) is a 0x40-byte opaque blob, so shadPS4's nonnull check and `size == sizeof` reject ([81be949167d0060b08f26dc3e055a0b640d129c8](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp)) cannot be applied without inventing a layout) and no async kind or completion state is stored until the console's contract for them is evidenced ([#9277](https://github.com/boykopovar/AnyPS5/issues/9277)) From ee785992dde2f8522328ab3b05d845f61f6d3518 Mon Sep 17 00:00:00 2001 From: Kovalev Vladislav Date: Wed, 28 Oct 2026 19:19:02 +0600 Subject: [PATCH 111/160] fix(tools): refuse mismatched SELF program header mappings --- tools/tests/test_unwrap_self.py | 75 ++++++++++++++++++++++++++++++--- tools/unwrap_self.py | 27 +++++++----- 2 files changed, 87 insertions(+), 15 deletions(-) diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index 250120681a..2658c2e014 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -884,16 +884,22 @@ class UnwrapTest(unittest.TestCase): def test_invalid_phdr_metadata_preserves_input_and_output(self): load = (PT_LOAD, 4, PAGE, 0x4000, 0x400, PAGE) phdr = (PT_PHDR, 4, PAGE, 0x4000, 2 * 56, 8) - cases = [([phdr, phdr, load], None), ([load, phdr], None)] - cases += [([(PT_PHDR, 4, PAGE, 0x4000, size, 8), load], None) for size in (0, 2 * 56 - 1)] - cases += [([phdr, load], size) for size in (0, 2 * 56 - 1)] + duplicate = (PT_PHDR, 4, PAGE, 0x4000, 3 * 56, 8) + cases = [([duplicate, duplicate, load], None, "more than one PT_PHDR segment"), + ([load, phdr], None, "PT_PHDR must precede the load segments")] + cases += [([(PT_PHDR, 4, PAGE, 0x4000, size, 8), load], 2 * 56, + "PT_PHDR does not cover the program header table") for size in (0, 2 * 56 - 1)] + cases += [([phdr, load], size, "PT_PHDR does not cover the program header table") + for size in (0, 2 * 56 - 1)] directory = Path(self.directory) source, output = directory / "input.self", directory / "output.elf" - for segments, memory in cases: + for segments, memory, message in cases: container = bytearray(build_container(segments, [(0x100800, 0x3000, 0x400)], {0x3000: (b"load data\0" * 128)[:0x400]}, program_offset=PAGE)) if memory is not None: struct.pack_into(" Date: Wed, 21 Oct 2026 22:19:52 -0700 Subject: [PATCH 112/160] feat(libScePs2EmuMenuDialog): return the measured firmware 13.42 results Co-Authored-By: Claude Sonnet 5.5 --- .../prx/libScePs2EmuMenuDialog/CMakeLists.txt | 14 ++++++ .../prx/libScePs2EmuMenuDialog/Export.cpp | 29 ++++++------ core/libs/tests/GuestPs2EmuMenuDialog.cpp | 45 +++++++++++++++++++ docs/dev/debt/libScePs2EmuMenuDialog.md | 2 +- 4 files changed, 74 insertions(+), 16 deletions(-) create mode 100644 core/libs/prx/libScePs2EmuMenuDialog/CMakeLists.txt create mode 100644 core/libs/tests/GuestPs2EmuMenuDialog.cpp diff --git a/core/libs/prx/libScePs2EmuMenuDialog/CMakeLists.txt b/core/libs/prx/libScePs2EmuMenuDialog/CMakeLists.txt new file mode 100644 index 0000000000..fac9e8f0bf --- /dev/null +++ b/core/libs/prx/libScePs2EmuMenuDialog/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(libScePs2EmuMenuDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libScePs2EmuMenuDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libScePs2EmuMenuDialog) + +if(BUILD_TESTING) + add_test_executable(guest_ps2_emu_menu_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestPs2EmuMenuDialog.cpp) + target_include_directories(guest_ps2_emu_menu_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ps2_emu_menu_dialog_tests PRIVATE libScePs2EmuMenuDialog) + add_test(NAME guest_ps2_emu_menu_dialog COMMAND guest_ps2_emu_menu_dialog_tests) + if(WIN32) + set_tests_properties(guest_ps2_emu_menu_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libScePs2EmuMenuDialog/Export.cpp b/core/libs/prx/libScePs2EmuMenuDialog/Export.cpp index a27fb265fc..d454772b97 100644 --- a/core/libs/prx/libScePs2EmuMenuDialog/Export.cpp +++ b/core/libs/prx/libScePs2EmuMenuDialog/Export.cpp @@ -2,43 +2,42 @@ #include #include #include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" + +namespace { + +constexpr int PS2_EMU_MENU_DIALOG_ERROR_UNAVAILABLE = static_cast(0x80B8000Fu); +constexpr int COMMON_DIALOG_STATUS_NONE = 0; + +} extern "C" { int APS5_VABI scePs2EmuMenuDialogClose() { - NotImplemented_nid_no_patch(__func__); - return 0; + return PS2_EMU_MENU_DIALOG_ERROR_UNAVAILABLE; } int APS5_VABI scePs2EmuMenuDialogGetResult() { - NotImplemented_nid_no_patch(__func__); - return 0; + return PS2_EMU_MENU_DIALOG_ERROR_UNAVAILABLE; } int APS5_VABI scePs2EmuMenuDialogGetStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; + return COMMON_DIALOG_STATUS_NONE; } int APS5_VABI scePs2EmuMenuDialogInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; + return PS2_EMU_MENU_DIALOG_ERROR_UNAVAILABLE; } int APS5_VABI scePs2EmuMenuDialogOpen() { - NotImplemented_nid_no_patch(__func__); - return 0; + return PS2_EMU_MENU_DIALOG_ERROR_UNAVAILABLE; } int APS5_VABI scePs2EmuMenuDialogTerminate() { - NotImplemented_nid_no_patch(__func__); - return 0; + return PS2_EMU_MENU_DIALOG_ERROR_UNAVAILABLE; } int APS5_VABI scePs2EmuMenuDialogUpdateStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; + return COMMON_DIALOG_STATUS_NONE; } } diff --git a/core/libs/tests/GuestPs2EmuMenuDialog.cpp b/core/libs/tests/GuestPs2EmuMenuDialog.cpp new file mode 100644 index 0000000000..cb6f6e2fd3 --- /dev/null +++ b/core/libs/tests/GuestPs2EmuMenuDialog.cpp @@ -0,0 +1,45 @@ +#include "prx/libScePs2EmuMenuDialog/include/Api.hpp" +#include +#include +#include +#include + +namespace { + +constexpr int kStatusNone = 0; +constexpr int kErrUnavailable = static_cast(0x80B8000Fu); + +using OneArg = int(APS5_VABI*)(void*); + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "PS2EmuMenuDialog: %s\n", message); + std::abort(); + } +} + +void CheckUntouched(OneArg fn, int expected, const char* message) { + std::uint8_t buffer[32]; + for (std::size_t i = 0; i < sizeof(buffer); ++i) buffer[i] = static_cast(0xA5 ^ i); + void* const args[] = {nullptr, buffer, reinterpret_cast(~std::uintptr_t{0})}; + for (void* arg : args) { + Require(fn(arg) == expected, message); + } + for (std::size_t i = 0; i < sizeof(buffer); ++i) { + Require(buffer[i] == static_cast(0xA5 ^ i), message); + } +} + +} + +int main() { + for (int round = 0; round < 2; ++round) { + CheckUntouched(reinterpret_cast(&scePs2EmuMenuDialogInitialize), kErrUnavailable, "initialize"); + CheckUntouched(reinterpret_cast(&scePs2EmuMenuDialogOpen), kErrUnavailable, "open"); + CheckUntouched(reinterpret_cast(&scePs2EmuMenuDialogGetResult), kErrUnavailable, "get result"); + CheckUntouched(reinterpret_cast(&scePs2EmuMenuDialogClose), kErrUnavailable, "close"); + CheckUntouched(reinterpret_cast(&scePs2EmuMenuDialogTerminate), kErrUnavailable, "terminate"); + CheckUntouched(reinterpret_cast(&scePs2EmuMenuDialogGetStatus), kStatusNone, "get status"); + CheckUntouched(reinterpret_cast(&scePs2EmuMenuDialogUpdateStatus), kStatusNone, "update status"); + } +} diff --git a/docs/dev/debt/libScePs2EmuMenuDialog.md b/docs/dev/debt/libScePs2EmuMenuDialog.md index abb7485619..abebbe9810 100644 --- a/docs/dev/debt/libScePs2EmuMenuDialog.md +++ b/docs/dev/debt/libScePs2EmuMenuDialog.md @@ -2,4 +2,4 @@ ### Unknown function info -- [scePs2EmuMenuDialogClose, scePs2EmuMenuDialogGetResult, scePs2EmuMenuDialogGetStatus, scePs2EmuMenuDialogInitialize, scePs2EmuMenuDialogOpen, scePs2EmuMenuDialogTerminate and scePs2EmuMenuDialogUpdateStatus](../../../core/libs/prx/libScePs2EmuMenuDialog/Export.cpp) (libScePs2EmuMenuDialog) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePs2EmuMenuDialog.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [scePs2EmuMenuDialogClose, scePs2EmuMenuDialogGetResult, scePs2EmuMenuDialogGetStatus, scePs2EmuMenuDialogInitialize, scePs2EmuMenuDialogOpen, scePs2EmuMenuDialogTerminate and scePs2EmuMenuDialogUpdateStatus](../../../core/libs/prx/libScePs2EmuMenuDialog/Export.cpp) (libScePs2EmuMenuDialog) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePs2EmuMenuDialog.c). Behaviour follows a measurement on firmware 13.42 reported in PR #9634: `Initialize`, `Open`, `GetResult`, `Close` and `Terminate` always return `0x80B8000F` and `UpdateStatus`/`GetStatus` return 0 (`COMMON_DIALOG_STATUS_NONE`), with no state and no writes to the argument, checked with null, small and all-ones arguments. Other firmware versions and the parameter types remain unmeasured; the declarations have no parameters and this is not an ABI claim. From 283be2c5c29270d9d9d49f5aeb28ca02506fc6ef Mon Sep 17 00:00:00 2001 From: Delcio Date: Sun, 18 Oct 2026 22:37:17 +0100 Subject: [PATCH 113/160] fix(libScePlayerInvitationDialog): return standard common-dialog error codes --- .../libScePlayerInvitationDialog.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp b/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp index 36965cc8e4..96073c0736 100644 --- a/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp +++ b/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp @@ -8,7 +8,9 @@ namespace { constexpr std::int32_t COMMON_DIALOG_RESULT_USER_CANCELED = 1; constexpr auto COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr auto COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); constexpr auto COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); +constexpr auto COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); constexpr auto COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); std::mutex g_dialog_mutex; @@ -25,19 +27,18 @@ extern "C" { std::int32_t APS5_VABI scePlayerInvitationDialogInitialize(void) { std::lock_guard lock(g_dialog_mutex); APS5_LOG_OUT("status=%d", status()); - if (g_dialog_status == PlayerInvitationDialogStatus::None) { - g_dialog_status = PlayerInvitationDialogStatus::Initialized; - } + if (g_dialog_status != PlayerInvitationDialogStatus::None) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + g_dialog_status = PlayerInvitationDialogStatus::Initialized; return 0; } std::int32_t APS5_VABI scePlayerInvitationDialogOpen(const ScePlayerInvitationDialogParam* param) { std::lock_guard lock(g_dialog_mutex); APS5_LOG_OUT("param=%p status=%d", static_cast(param), status()); - if (g_dialog_status == PlayerInvitationDialogStatus::None) { - g_dialog_status = PlayerInvitationDialogStatus::Initialized; - } - g_dialog_status = PlayerInvitationDialogStatus::Running; + if (g_dialog_status == PlayerInvitationDialogStatus::None) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (g_dialog_status == PlayerInvitationDialogStatus::Running) return COMMON_DIALOG_ERROR_BUSY; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + g_dialog_status = PlayerInvitationDialogStatus::Finished; return 0; } @@ -68,6 +69,7 @@ std::int32_t APS5_VABI scePlayerInvitationDialogClose(void) { std::int32_t APS5_VABI scePlayerInvitationDialogTerminate(void) { std::lock_guard lock(g_dialog_mutex); APS5_LOG_OUT("status=%d", status()); + if (g_dialog_status == PlayerInvitationDialogStatus::None) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; g_dialog_status = PlayerInvitationDialogStatus::None; return 0; } From 5782e6d6c1859d62f7c295036eb533ac60a42f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= Date: Wed, 28 Oct 2026 14:23:48 +0100 Subject: [PATCH 114/160] fix(libScePlayerInvitationDialog): keep Open running until UpdateStatus, check param first, update test and debt Co-Authored-By: Claude Sonnet 5.5 --- .../libScePlayerInvitationDialog.cpp | 6 ++---- core/libs/tests/PlayerInvitationDialog.cpp | 16 ++++++++++++---- docs/dev/debt/libScePlayerInvitationDialog.md | 1 + 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp b/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp index 96073c0736..04a46049eb 100644 --- a/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp +++ b/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp @@ -10,7 +10,6 @@ constexpr std::int32_t COMMON_DIALOG_RESULT_USER_CANCELED = 1; constexpr auto COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); constexpr auto COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); constexpr auto COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); -constexpr auto COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); constexpr auto COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); std::mutex g_dialog_mutex; @@ -35,10 +34,9 @@ std::int32_t APS5_VABI scePlayerInvitationDialogInitialize(void) { std::int32_t APS5_VABI scePlayerInvitationDialogOpen(const ScePlayerInvitationDialogParam* param) { std::lock_guard lock(g_dialog_mutex); APS5_LOG_OUT("param=%p status=%d", static_cast(param), status()); - if (g_dialog_status == PlayerInvitationDialogStatus::None) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - if (g_dialog_status == PlayerInvitationDialogStatus::Running) return COMMON_DIALOG_ERROR_BUSY; if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; - g_dialog_status = PlayerInvitationDialogStatus::Finished; + if (g_dialog_status == PlayerInvitationDialogStatus::None) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + g_dialog_status = PlayerInvitationDialogStatus::Running; return 0; } diff --git a/core/libs/tests/PlayerInvitationDialog.cpp b/core/libs/tests/PlayerInvitationDialog.cpp index 4cbd370640..f023a4b7ab 100644 --- a/core/libs/tests/PlayerInvitationDialog.cpp +++ b/core/libs/tests/PlayerInvitationDialog.cpp @@ -16,6 +16,7 @@ void expect(std::int32_t actual, std::int32_t expected) { int main() { using enum PlayerInvitationDialogStatus; constexpr auto notInitialized = static_cast(0x80B80003u); + constexpr auto alreadyInitialized = static_cast(0x80B80004u); constexpr auto notFinished = static_cast(0x80B80005u); constexpr auto argNull = static_cast(0x80B8000Du); constexpr std::int32_t userCanceled = 1; @@ -23,16 +24,19 @@ int main() { expect(scePlayerInvitationDialogGetResult(&result), notInitialized); expect(scePlayerInvitationDialogUpdateStatus(), static_cast(None)); expect(scePlayerInvitationDialogClose(), 0); - expect(scePlayerInvitationDialogTerminate(), 0); + expect(scePlayerInvitationDialogTerminate(), notInitialized); + expect(scePlayerInvitationDialogOpen(nullptr), argNull); expect(scePlayerInvitationDialogInitialize(), 0); - expect(scePlayerInvitationDialogInitialize(), 0); + expect(scePlayerInvitationDialogInitialize(), alreadyInitialized); expect(scePlayerInvitationDialogGetStatus(), static_cast(Initialized)); expect(scePlayerInvitationDialogUpdateStatus(), static_cast(Initialized)); expect(scePlayerInvitationDialogGetResult(nullptr), argNull); expect(scePlayerInvitationDialogGetResult(&result), notFinished); ScePlayerInvitationDialogParam param{}; + expect(scePlayerInvitationDialogOpen(nullptr), argNull); + expect(scePlayerInvitationDialogGetStatus(), static_cast(Initialized)); expect(scePlayerInvitationDialogOpen(¶m), 0); expect(scePlayerInvitationDialogGetStatus(), static_cast(Running)); expect(scePlayerInvitationDialogGetResult(&result), notFinished); @@ -43,14 +47,18 @@ int main() { expect(result.result, userCanceled); for (const std::uint8_t byte : result.reserved) expect(byte, 0); expect(scePlayerInvitationDialogUpdateStatus(), static_cast(Finished)); - expect(scePlayerInvitationDialogOpen(nullptr), 0); + expect(scePlayerInvitationDialogOpen(nullptr), argNull); expect(scePlayerInvitationDialogClose(), 0); expect(scePlayerInvitationDialogGetStatus(), static_cast(Finished)); expect(scePlayerInvitationDialogTerminate(), 0); + expect(scePlayerInvitationDialogTerminate(), notInitialized); expect(scePlayerInvitationDialogGetStatus(), static_cast(None)); expect(scePlayerInvitationDialogGetResult(&result), notInitialized); - expect(scePlayerInvitationDialogOpen(nullptr), 0); + expect(scePlayerInvitationDialogOpen(¶m), notInitialized); + expect(scePlayerInvitationDialogInitialize(), 0); + expect(scePlayerInvitationDialogOpen(¶m), 0); + expect(scePlayerInvitationDialogGetStatus(), static_cast(Running)); expect(scePlayerInvitationDialogUpdateStatus(), static_cast(Finished)); expect(scePlayerInvitationDialogTerminate(), 0); } diff --git a/docs/dev/debt/libScePlayerInvitationDialog.md b/docs/dev/debt/libScePlayerInvitationDialog.md index 464662bb92..10f35fafd4 100644 --- a/docs/dev/debt/libScePlayerInvitationDialog.md +++ b/docs/dev/debt/libScePlayerInvitationDialog.md @@ -7,3 +7,4 @@ ### Functional - [libScePlayerInvitationDialog](../../../core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp) simulates dialog completion without displaying UI or sending invitations, so `scePlayerInvitationDialogGetResult` reports `USER_CANCELED`; its parameter ABI remains unverified. The 40-byte result layout (error code, result, 32 reserved bytes) is taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Dialog/PlayerInvitationDialog.cs) and the error codes from the other common dialogs, not confirmed on a title. +- [scePlayerInvitationDialogInitialize, scePlayerInvitationDialogOpen, scePlayerInvitationDialogTerminate](../../../core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp) (libScePlayerInvitationDialog) - the codes `NOT_INITIALIZED`, `ALREADY_INITIALIZED` and `ARG_NULL`, and the order of the `Open` checks (param first), were measured on the firmware 13.42 module. `Open` does not validate the contents of the param (size 0x88, user, mode, string at `+0x40`, zeroed tail), which the module rejects with `0x80B8000A`, does not return `BUSY` for `Open` while running, and `Initialize` does not return `0x80B80008` while another common dialog is in use. From ae305dfd5d11c66483a47ee2d71b8ecb74635a6a Mon Sep 17 00:00:00 2001 From: Kian Date: Wed, 28 Oct 2026 23:05:17 +1000 Subject: [PATCH 115/160] feat(agc): APS5_DUMP_SHADERS takes a list of program addresses A comma-separated list of hex program addresses (optional 0x prefix) now writes only those programs' requests, so one expensive shader can be captured without serializing every request a title prepares. Any other value, such as 1, keeps writing every request. A filtered-out program returns no name, so diagnostics never point at a file that was not written, and it stays eligible for a later list. Co-Authored-By: Claude Opus 5.5 --- .../src/Driver/Shaders/ShaderDiagnostics.cpp | 26 +++++++++++ .../tests/execution/PreparedShaders.cpp | 45 +++++++++++++++++++ docs/dev/ENVIRONMENT.md | 2 +- 3 files changed, 72 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp index 91b97871d7..19a1ca8811 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp @@ -1,12 +1,37 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" #include "ControlFlow/RequestSerializer.hpp" +#include #include #include +#include #include namespace AgcDriver::DriverDetail { +namespace { + +std::optional> selectedPrograms() { + const char* value = std::getenv("APS5_DUMP_SHADERS"); + if (value == nullptr || std::string_view(value) == "1") return std::nullopt; + std::set programs; + std::string_view rest(value); + while (!rest.empty()) { + const auto comma = rest.find(','); + auto token = rest.substr(0, comma); + rest = comma == std::string_view::npos ? std::string_view{} : rest.substr(comma + 1); + if (token.starts_with("0x") || token.starts_with("0X")) token.remove_prefix(2); + std::uint64_t address = 0; + const auto [end, error] = std::from_chars(token.data(), token.data() + token.size(), address, 16); + if (token.empty() || error != std::errc{} || end != token.data() + token.size()) return std::nullopt; + programs.insert(address); + } + if (programs.empty()) return std::nullopt; + return programs; +} + +} + void Driver::reportSkip(const char* kind, const std::string& what) { static AgcDriver::Mutex reportedMutex; static std::set reported; @@ -56,6 +81,7 @@ std::string Driver::dumpRequest(std::uint64_t address, const ShaderRecompiler::R char hex[32]; std::snprintf(hex, sizeof(hex), "%llx", static_cast(address)); const std::string name = "shader_" + std::string(hex) + std::string(suffix) + ".req"; + if (const auto selected = selectedPrograms(); selected.has_value() && !selected->contains(address)) return {}; std::lock_guard lock(dumpMutex); if (!dumped.insert(name).second) return name; try { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index 94b7c946eb..1e5cd00571 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -195,6 +195,50 @@ void Run(AgcDriver::VulkanDevice& device) { ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::SourceHandleFor(snapshot, 0, request)); }, "does not refer to registered code"); } +void DumpSelection(AgcDriver::VulkanDevice& device) { + using namespace ShaderRecompiler; + const std::array code{0xffffffffu}; + const std::vector header(2048, std::byte{0x5a}); + RecompileRequest request{{ShaderStage::Compute, 0x12345cafeull, code, 0x20000, header}, + {32, 0, {}, ShaderComputeStageInfo{{1, 1, 1}, 0, {}, false, 1, {}}, {}, {}, {}}, device.ComputeTarget(32), {0, 0, 0, 128}}; + const char* previous = std::getenv("APS5_DUMP_SHADERS"); + const std::string saved = previous != nullptr ? previous : ""; + const auto setCapture = [](const char* value) { +#ifdef _WIN32 + Require(_putenv_s("APS5_DUMP_SHADERS", value) == 0, "cannot set shader capture"); +#else + Require(setenv("APS5_DUMP_SHADERS", value, 1) == 0, "cannot set shader capture"); +#endif + }; + const auto written = [](const std::string& name) { + const bool exists = std::ifstream(name, std::ios::binary).is_open(); + std::remove(name.c_str()); + return exists; + }; + const std::string excludedPath = "shader_12345cafe_excluded.req"; + const std::string listedPath = "shader_12345cafe_listed.req"; + const std::string otherPath = "shader_feedface_unlisted.req"; + std::remove(excludedPath.c_str()); + std::remove(listedPath.c_str()); + std::remove(otherPath.c_str()); + setCapture("feedface"); + Require(AgcDriver::DriverDetail::Driver::dumpRequest(request.shader.codeAddress, request, "_excluded").empty(), "a program outside the APS5_DUMP_SHADERS list was named as dumped"); + Require(!written(excludedPath), "a program outside the APS5_DUMP_SHADERS list was dumped"); + setCapture("feedface,0x12345CAFE"); + Require(AgcDriver::DriverDetail::Driver::dumpRequest(request.shader.codeAddress, request, "_excluded") == excludedPath, "a program dropped by an earlier list was not dumped once listed"); + Require(written(excludedPath), "a listed program's request was not written after an earlier list dropped it"); + Require(AgcDriver::DriverDetail::Driver::dumpRequest(request.shader.codeAddress, request, "_listed") == listedPath, "a listed program was not dumped"); + Require(written(listedPath), "a listed program's request was not written"); + setCapture("12345cafe,not-an-address"); + Require(AgcDriver::DriverDetail::Driver::dumpRequest(0xfeedfaceull, request, "_unlisted") == otherPath, "a value that is not an address list did not dump every program"); + Require(written(otherPath), "a value that is not an address list did not write the request"); +#ifdef _WIN32 + Require(_putenv_s("APS5_DUMP_SHADERS", saved.c_str()) == 0, "cannot restore shader capture"); +#else + Require((previous != nullptr ? setenv("APS5_DUMP_SHADERS", saved.c_str(), 1) : unsetenv("APS5_DUMP_SHADERS")) == 0, "cannot restore shader capture"); +#endif +} + void FailureCapture(AgcDriver::VulkanDevice& device) { using namespace ShaderRecompiler; const std::array code{0xffffffffu}; @@ -1543,6 +1587,7 @@ int main(int argc, char** argv) { Run(*device); RegisteredVertexWorkgroup(device->Target()); FailureCapture(*device); + DumpSelection(*device); PendingRegistrationPreparation(*device); AwaitedRegistrationPreparation(); Require(argc != 2 || std::string_view(argv[1]) != "--fail-before-registration", "injected failure before registration"); diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index b74ec48890..cd7a74ae73 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -148,7 +148,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_DUMP_QUEUE` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | dumps the first submissions of the queue with this hex id | | `APS5_DUMP_REJECTED` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/CommandValidation.cpp` | | writes a rejected submission to rejected_submission_.bin | | `APS5_DUMP_SCALE` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 4 | keeps every n-th pixel in each direction of a dumped frame (1 to 16384; any other value fails) | -| `APS5_DUMP_SHADERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | writes each prepared shader's request to shader_
.req | +| `APS5_DUMP_SHADERS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | writes each prepared shader's request to shader_
.req; a comma-separated list of hex program addresses (for example `807b90a100,0x807b925400`) writes only those programs' requests, and any other value (such as `1`) writes every request | | `APS5_DUMP_STORAGE` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | selects storage textures to dump by hex address or WxHxL[:format] extent as storage_
_.raw | | `APS5_DUMP_STORAGE_GATE` | path | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | with `APS5_DUMP_STORAGE`, waits for this path to exist and reads matching live images directly without guest writeback; at most eight addresses, 8 MiB linear storage per image; writes `storage_direct_
_.raw` with the existing pitch, height and VkFormat header and first-mip pixels | | `APS5_TEST_DIRECT_READBACK` | set | `core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp` | | runs the clean-volume gated live-image readback regression in the layered volume execution test | From 5a582ed23505fafe72dc6216683c75d4d7fa1f52 Mon Sep 17 00:00:00 2001 From: Omar Elmahy Date: Wed, 28 Oct 2026 13:26:50 +0000 Subject: [PATCH 116/160] test(libSceVrSetupDialog): cover parameter validation precedence over uninitialized state --- core/libs/tests/GuestVrSetupDialog.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/libs/tests/GuestVrSetupDialog.cpp b/core/libs/tests/GuestVrSetupDialog.cpp index 4f106e22b9..e1593d7a4f 100644 --- a/core/libs/tests/GuestVrSetupDialog.cpp +++ b/core/libs/tests/GuestVrSetupDialog.cpp @@ -27,6 +27,20 @@ int main() { Require(sceVrSetupDialogTerminate() == kErrNotInitialized); Require(sceVrSetupDialogOpen(nullptr) == kErrArgNull); Require(sceVrSetupDialogOpen(¶m) == kErrNotInitialized); + VrSetupDialogParam invalidParam = param; + invalidParam.size = 0; + Require(sceVrSetupDialogOpen(&invalidParam) == kErrArgNull); + invalidParam.size = 55; + Require(sceVrSetupDialogOpen(&invalidParam) == kErrArgNull); + invalidParam.size = 57; + Require(sceVrSetupDialogOpen(&invalidParam) == kErrArgNull); + invalidParam.size = sizeof(param); + invalidParam.reserved8 = 1; + Require(sceVrSetupDialogOpen(&invalidParam) == kErrArgNull); + invalidParam.reserved8 = 0; + invalidParam.reserved9 = 1; + Require(sceVrSetupDialogOpen(&invalidParam) == kErrArgNull); + invalidParam.reserved9 = 0; Require(sceVrSetupDialogClose() == kErrNotInitialized); Require(sceVrSetupDialogGetResult(nullptr) == kErrArgNull); Require(sceVrSetupDialogGetResult(result) == kErrNotInitialized); From b0ed32bd6c3d02643c4effaba08ed0172b69aece Mon Sep 17 00:00:00 2001 From: InfinitySpeed <101837966+InfinitySpeed@users.noreply.github.com> Date: Sat, 24 Oct 2026 02:19:20 +0200 Subject: [PATCH 117/160] feat(libSceConvertKeycode): implement sceConvertKeycodeGetCharacterFromKeyboardData --- core/libs/prx/libSceConvertKeycode/Export.cpp | 9 +++++- core/libs/tests/GuestConvertKeycode.cpp | 30 +++++++++++++++++++ docs/dev/debt/libSceConvertKeycode.md | 3 +- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceConvertKeycode/Export.cpp b/core/libs/prx/libSceConvertKeycode/Export.cpp index f96e2deca1..fa2cf6af55 100644 --- a/core/libs/prx/libSceConvertKeycode/Export.cpp +++ b/core/libs/prx/libSceConvertKeycode/Export.cpp @@ -2431,7 +2431,14 @@ int APS5_VABI sceConvertKeycodeGetVirtualKeycode(std::uint16_t keycode, std::uin } int APS5_VABI sceConvertKeycodeGetCharacterFromKeyboardData(const KeyboardData* keyData, std::int32_t index, std::uint32_t state, std::uint32_t keyboardType, char16_t* character) { - NotImplemented_nid_no_patch(__func__); + if (keyData == nullptr || character == nullptr) return kErrInvalidAddress; + if (keyboardType == 0 || keyboardType > 39) return kErrInvalidKeyboardType; + if (keyboardType >= 31) throw std::runtime_error("sceConvertKeycodeGetCharacterFromKeyboardData: guest sdk version is unavailable"); + if (index < 0 || index >= keyData->length || index >= static_cast(KEYBOARD_MAX_KEYCODES)) { + throw std::runtime_error("sceConvertKeycodeGetCharacterFromKeyboardData: result for an index outside the key data is unknown"); + } + const std::uint16_t keycode = keyData->key_code[index]; + *character = ConvertCharacter(keycode, state, keyboardType); return 0; } diff --git a/core/libs/tests/GuestConvertKeycode.cpp b/core/libs/tests/GuestConvertKeycode.cpp index d2d3cb463f..fc100b607b 100644 --- a/core/libs/tests/GuestConvertKeycode.cpp +++ b/core/libs/tests/GuestConvertKeycode.cpp @@ -213,5 +213,35 @@ int main() { Require(sceConvertKeycodeGetCharacter(key, 0xFFFFFFFFu, layout, &character) == 0 && character == 0); } } + KeyboardData keyData{}; + keyData.length = 1; + keyData.key_code[0] = 4; + char16_t kbdChar = u'?'; + Require(sceConvertKeycodeGetCharacterFromKeyboardData(nullptr, 0, 0, 4, &kbdChar) == static_cast(0x80BC0031u)); + Require(sceConvertKeycodeGetCharacterFromKeyboardData(&keyData, 0, 0, 4, nullptr) == static_cast(0x80BC0031u)); + for (std::int32_t badIndex : {-1, 1, 16}) { + kbdChar = u'?'; + bool rejected = false; + try { + sceConvertKeycodeGetCharacterFromKeyboardData(&keyData, badIndex, 0, 4, &kbdChar); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected && kbdChar == u'?'); + } + for (std::uint32_t layout : {31u, 39u}) { + kbdChar = u'?'; + bool rejected = false; + try { + sceConvertKeycodeGetCharacterFromKeyboardData(&keyData, 0, 0, layout, &kbdChar); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected && kbdChar == u'?'); + } + Require(sceConvertKeycodeGetCharacterFromKeyboardData(&keyData, 0, 0, 0, &kbdChar) == static_cast(0x80BC001Du)); + Require(sceConvertKeycodeGetCharacterFromKeyboardData(&keyData, 0, 0, 40, &kbdChar) == static_cast(0x80BC001Du)); + Require(sceConvertKeycodeGetCharacterFromKeyboardData(&keyData, 0, 0, 4, &kbdChar) == 0 && kbdChar == u'a'); + Require(sceConvertKeycodeGetCharacterFromKeyboardData(&keyData, 0, 0x200, 4, &kbdChar) == 0 && kbdChar == u'A'); return 0; } diff --git a/docs/dev/debt/libSceConvertKeycode.md b/docs/dev/debt/libSceConvertKeycode.md index 2c6bae6882..3fbf0e754c 100644 --- a/docs/dev/debt/libSceConvertKeycode.md +++ b/docs/dev/debt/libSceConvertKeycode.md @@ -2,5 +2,6 @@ ### Functional -- [sceConvertKeycodeGetVirtualKeycode](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) and `sceConvertKeycodeGetCharacter`: layouts 31–39 throw until the guest platform and compiled SDK version are exposed. `GetVirtualKeycode` also throws for version-dependent Y/Z mappings in layouts 2, 3 and 12. Mapping tables and modifier behavior are recovered; no SDK version is guessed. Three titles disassembled for this entry call `sceConvertKeycodeGetVirtualKeycode` at four sites (PPSA24779 0xa9199a, PPSA29866 0x2d8bdd, PPSA30601 0x15e4624 and 0x15e47da): the keyboard type is the constant 4 at 0xa9199a and 0x15e47da and a run-time value at the other two, and no value read is 2, 3, 12 or in 31 to 39. +- [sceConvertKeycodeGetVirtualKeycode](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) `sceConvertKeycodeGetCharacter` and `sceConvertKeycodeGetCharacterFromKeyboardData`: layouts 31–39 throw until the guest platform and compiled SDK version are exposed. `GetVirtualKeycode` also throws for version-dependent Y/Z mappings in layouts 2, 3 and 12. Mapping tables and modifier behavior are recovered; no SDK version is guessed. Three titles disassembled for this entry call `sceConvertKeycodeGetVirtualKeycode` at four sites (PPSA24779 0xa9199a, PPSA29866 0x2d8bdd, PPSA30601 0x15e4624 and 0x15e47da): the keyboard type is the constant 4 at 0xa9199a and 0x15e47da and a run-time value at the other two, and no value read is 2, 3, 12 or in 31 to 39. +- [sceConvertKeycodeGetCharacterFromKeyboardData](../../../core/libs/prx/libSceConvertKeycode/Export.cpp): an index outside `keyData->length` or `KEYBOARD_MAX_KEYCODES` throws; no reference gives the real result or validation order. - [sceConvertKeycodeGetImeKeyboardType](../../../core/libs/prx/libSceConvertKeycode/Export.cpp): implemented exactly as the repository owner (boykopovar) specified the UserService keyboard type (plus the Japanese variant for type 37 and the Traditional Chinese variant for type 39) to IME keyboard type table, the SDK compatibility rules (38 becomes 4 for PS5 SDK below 0x06000000 or PS4 SDK below 0x10000000; 39 becomes 29 for PS5 SDK below 0x08000000 and for every PS4 client), the error codes and the unmatched-type result (success, IME type 0). The PS4/PS5 client rule reads the title ID prefix from `param.sfo` or `param.json` (`CUSA` is PS4); a missing, malformed or oversized ID or title, as in homebrew, is treated as PS5. Reliable detection inspects the original `eboot.bin`, which only the relinker reads, so it is not done. UserService reports the keyboard type of the configured system language, so the function returns the owner's IME type for that language (see libSceUserService.md). From 0a071016498f3a6b09a6bfb4c11fd448c046c5bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= Date: Wed, 28 Oct 2026 14:27:12 +0100 Subject: [PATCH 118/160] docs(libSceConvertKeycode): fix debt entry punctuation --- 3rdparty/SPIRV-Headers | 2 +- 3rdparty/SPIRV-Tools | 2 +- 3rdparty/vorbis | 2 +- docs/dev/debt/libSceConvertKeycode.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/3rdparty/SPIRV-Headers b/3rdparty/SPIRV-Headers index b824a462d4..01e0577914 160000 --- a/3rdparty/SPIRV-Headers +++ b/3rdparty/SPIRV-Headers @@ -1 +1 @@ -Subproject commit b824a462d4256d720bebb40e78b9eb8f78bbb305 +Subproject commit 01e0577914a75a2569c846778c2f93aa8e6feddd diff --git a/3rdparty/SPIRV-Tools b/3rdparty/SPIRV-Tools index 262bdab481..7f2d9ee926 160000 --- a/3rdparty/SPIRV-Tools +++ b/3rdparty/SPIRV-Tools @@ -1 +1 @@ -Subproject commit 262bdab48146c937467f826699a40da0fdfc0f1a +Subproject commit 7f2d9ee926f98fc77a3ed1e1e0f113b8c9c49458 diff --git a/3rdparty/vorbis b/3rdparty/vorbis index c2aa86b05e..0657aee69d 160000 --- a/3rdparty/vorbis +++ b/3rdparty/vorbis @@ -1 +1 @@ -Subproject commit c2aa86b05e981c96bf381fc6aa11cdd03eccc2fb +Subproject commit 0657aee69dec8508a0011f47f3b69d7538e9d262 diff --git a/docs/dev/debt/libSceConvertKeycode.md b/docs/dev/debt/libSceConvertKeycode.md index 3fbf0e754c..22a13f06a2 100644 --- a/docs/dev/debt/libSceConvertKeycode.md +++ b/docs/dev/debt/libSceConvertKeycode.md @@ -2,6 +2,6 @@ ### Functional -- [sceConvertKeycodeGetVirtualKeycode](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) `sceConvertKeycodeGetCharacter` and `sceConvertKeycodeGetCharacterFromKeyboardData`: layouts 31–39 throw until the guest platform and compiled SDK version are exposed. `GetVirtualKeycode` also throws for version-dependent Y/Z mappings in layouts 2, 3 and 12. Mapping tables and modifier behavior are recovered; no SDK version is guessed. Three titles disassembled for this entry call `sceConvertKeycodeGetVirtualKeycode` at four sites (PPSA24779 0xa9199a, PPSA29866 0x2d8bdd, PPSA30601 0x15e4624 and 0x15e47da): the keyboard type is the constant 4 at 0xa9199a and 0x15e47da and a run-time value at the other two, and no value read is 2, 3, 12 or in 31 to 39. +- [sceConvertKeycodeGetVirtualKeycode](../../../core/libs/prx/libSceConvertKeycode/Export.cpp), `sceConvertKeycodeGetCharacter` and `sceConvertKeycodeGetCharacterFromKeyboardData`: layouts 31–39 throw until the guest platform and compiled SDK version are exposed. `GetVirtualKeycode` also throws for version-dependent Y/Z mappings in layouts 2, 3 and 12. Mapping tables and modifier behavior are recovered; no SDK version is guessed. Three titles disassembled for this entry call `sceConvertKeycodeGetVirtualKeycode` at four sites (PPSA24779 0xa9199a, PPSA29866 0x2d8bdd, PPSA30601 0x15e4624 and 0x15e47da): the keyboard type is the constant 4 at 0xa9199a and 0x15e47da and a run-time value at the other two, and no value read is 2, 3, 12 or in 31 to 39. - [sceConvertKeycodeGetCharacterFromKeyboardData](../../../core/libs/prx/libSceConvertKeycode/Export.cpp): an index outside `keyData->length` or `KEYBOARD_MAX_KEYCODES` throws; no reference gives the real result or validation order. - [sceConvertKeycodeGetImeKeyboardType](../../../core/libs/prx/libSceConvertKeycode/Export.cpp): implemented exactly as the repository owner (boykopovar) specified the UserService keyboard type (plus the Japanese variant for type 37 and the Traditional Chinese variant for type 39) to IME keyboard type table, the SDK compatibility rules (38 becomes 4 for PS5 SDK below 0x06000000 or PS4 SDK below 0x10000000; 39 becomes 29 for PS5 SDK below 0x08000000 and for every PS4 client), the error codes and the unmatched-type result (success, IME type 0). The PS4/PS5 client rule reads the title ID prefix from `param.sfo` or `param.json` (`CUSA` is PS4); a missing, malformed or oversized ID or title, as in homebrew, is treated as PS5. Reliable detection inspects the original `eboot.bin`, which only the relinker reads, so it is not done. UserService reports the keyboard type of the configured system language, so the function returns the owner's IME type for that language (see libSceUserService.md). From 4ababcbcfa7218122c5d1f19c639766c7796f1d6 Mon Sep 17 00:00:00 2001 From: dom Date: Wed, 28 Oct 2026 09:35:23 -0400 Subject: [PATCH 119/160] fix(tools): ignore no-op relocations in module annotations --- tools/annotate_module.py | 3 ++ tools/tests/test_annotate_module.py | 58 +++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/tools/annotate_module.py b/tools/annotate_module.py index a4ef16d003..7074dc3593 100644 --- a/tools/annotate_module.py +++ b/tools/annotate_module.py @@ -32,6 +32,7 @@ STT_TLS = 6 DT_GNU_HASH = 0x6ffffef5 SYMBOL_SIZE = 24 RELA_SIZE = 24 +R_X86_64_NONE = 0 LINE_ADDRESS = re.compile(r"^\s*([0-9a-f]+):") STUB = re.compile(r"^\s*([0-9a-f]+):\s+(?:\w+\s+)?jmpq?\s+\*(?:-?0x[0-9a-f]+)?\(%rip\)\s+# 0x([0-9a-f]+)") @@ -159,6 +160,8 @@ class Module: start = self.address(table) for position in range(start, start + size, RELA_SIZE): offset, info, _ = self.unpack("> 32: slots[offset] = self.symbol(info >> 32)[0] return slots diff --git a/tools/tests/test_annotate_module.py b/tools/tests/test_annotate_module.py index 542e44b022..98354f6179 100644 --- a/tools/tests/test_annotate_module.py +++ b/tools/tests/test_annotate_module.py @@ -97,6 +97,44 @@ def instruction(listing, mnemonic): return next(line for line in listing if pattern.search(line)) +def with_relocation(module, table_offset, index, slot, symbol_index, kind): + image = bytearray(module) + struct.pack_into(" " + IMPORT_NAME)) + def test_none_relocation_does_not_annotate_a_stub_or_call(self): + module = with_relocation(build_module(), 0x2280, 0, SLOT, 1, 0) + summary = self.annotate(module) + self.assertIn("0 imports, 1 exports, 0 tls, 0 stubs", summary) + self.assertEqual(self.read(".imports.txt"), "") + listing = self.read(".asm") + self.assertNotIn("; GOT ", listing) + self.assertNotIn("; -> ", listing) + self.assertIn("; ===== export " + EXPORT_NAME, listing) + + def test_none_relocation_keeps_the_real_import_annotation(self): + symbols = DEFAULT_SYMBOLS + [(SECOND_IMPORT_NAME, 0, 0x12, 0)] + module = build_module(symbols=symbols, relocations=((SLOT, 1), (SLOT, 3))) + module = with_relocation(module, 0x2280, 1, SLOT, 3, 0) + self.annotate(module) + self.assertEqual(self.read(".imports.txt"), "%#x %s %s\n" % (SLOT, symbol_name(IMPORT_NAME), IMPORT_NAME)) + listing = self.read(".asm").splitlines() + self.assertTrue(instruction(listing, "jmp").endswith("; GOT " + IMPORT_NAME)) + self.assertTrue(instruction(listing, "call").endswith("; -> " + IMPORT_NAME)) + def test_tls_symbol_is_listed_but_not_an_address(self): symbols = DEFAULT_SYMBOLS + [(TLS_NAME, 1, 0x16, EXPORT_ADDRESS)] summary = self.annotate(build_module(symbols=symbols)) From 4620e29426e904841ccc9bb1d16363cfcc08919a Mon Sep 17 00:00:00 2001 From: cgrd <20777498+cgrd@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:48:58 -0700 Subject: [PATCH 120/160] fix(shader): drop vector stores through TYPE 2/3 and FORMAT 0 buffer V#s as measured A buffer V# whose TYPE is not 0 threw "buffer descriptor uses an unsupported type" for every access. Measured on a PS5 (CFI-1115B, firmware 12.20): through a V# with TYPE 2 or 3 and FORMAT 0 or a valid format, buffer_store_dword and buffer_store_format_x write nothing and don't fault, so a buffer a shader only stores to through one is bound as a null V#. Loads and atomics through those V#s leave the destination VGPR unchanged, which a null V# can't reproduce, so they still throw. Through a TYPE 0, FORMAT 0 V#, vector loads return 0 and stores are dropped, and those buffers are bound as a null V# too. TYPE 1 faults the GPU for every access measured and keeps throwing. Co-Authored-By: Claude Opus 5.5 --- .../execution/BufferDescriptorRuntime.cpp | 78 +++++++++++++++++++ .../Optimization/src/ResourceMaterializer.cpp | 23 ++++++ docs/dev/debt/shader.md | 1 + 3 files changed, 102 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp index 0a1624612e..0f8ea4f99b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp @@ -4,6 +4,7 @@ #include "prx/libc/include/GuestAllocations.hpp" #include "Optimization/ResourceProgram.hpp" #include "VulkanTestDevice.hpp" +#include #include #include #include @@ -177,6 +178,82 @@ void ScalarReads(AgcDriver::VulkanDevice& device) { GuestAllocations::Mutation().Remove(Input.data()); } +void DisabledDescriptors(AgcDriver::VulkanDevice& device) { + constexpr std::uint32_t Value = 0x12345678u; + alignas(256) static constexpr std::array formattedStore{0x7e0202ffu, Value, 0xe0102000u, 0x80000100u, 0xbf810000u}; + alignas(256) static constexpr std::array rawStore{0x34020082u, 0x7e0402ffu, Value, 0xe0701000u, 0x80000201u, 0xbf810000u}; + alignas(256) static constexpr std::array formattedLoad{0x34020082u, 0xe0002000u, 0x80000200u, 0xbf8c3f70u, 0xe0701000u, 0x80010201u, 0xbf810000u}; + alignas(256) static constexpr std::array atomicAdd{0x34020082u, 0xe0c81000u, 0x80000201u, 0xbf810000u}; + std::array userData{}; + auto request = Request(userData, device.ComputeTarget(Lanes), {}); + request.useCache = false; + const auto output = BufferDescriptor(Output.data(), sizeof(Output)); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const auto dispatch = [&](std::span code, const std::array& descriptor) { + request.shader.code = code; + request.shader.codeAddress = reinterpret_cast(code.data()); + std::copy(descriptor.begin(), descriptor.end(), userData.begin()); + Output.fill(Fill); + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1); + device.WaitIdle(); + }; + const auto all = [](std::uint32_t expected) { return std::all_of(Output.begin(), Output.end(), [expected](std::uint32_t value) { return value == expected; }); }; + const auto raw = [](const void* data, std::uint32_t bytes, std::uint32_t word3) { + auto descriptor = BufferDescriptor(data, bytes); + descriptor[3] = word3; + return descriptor; + }; + const auto strided = [](const void* data, std::uint32_t word3) { + auto descriptor = BufferDescriptor(data, Lanes); + descriptor[1] |= 4u << 16u; + descriptor[3] = word3; + return descriptor; + }; + std::array input{}; + std::memcpy(input.data(), Input.data(), sizeof(input)); + dispatch(Code, raw(Input.data(), InputBytes, 0x31016facu)); + Require(Output == input, "raw load through a TYPE 0 32_FLOAT V# did not read the buffer"); + dispatch(formattedLoad, strided(Input.data(), 0x31014facu)); + Require(Output == input, "formatted load through a TYPE 0 32_UINT V# did not read the buffer"); + dispatch(rawStore, raw(Output.data(), sizeof(Output), 0x31016facu)); + Require(all(Value), "raw store through a TYPE 0 32_FLOAT V# did not write every lane"); + dispatch(formattedStore, strided(Output.data(), 0x31016facu)); + Require(all(Value), "formatted store through a TYPE 0 32_FLOAT V# did not write every lane"); + dispatch(Code, raw(Input.data(), InputBytes, 0x31000facu)); + Require(all(0u), "raw load through a TYPE 0 FORMAT 0 V# did not return 0"); + dispatch(formattedLoad, strided(Input.data(), 0x31000facu)); + Require(all(0u), "formatted load through a TYPE 0 FORMAT 0 V# did not return 0"); + for (const auto word3 : {0x31000facu, 0x91000facu, 0x91016facu, 0x91b00facu, 0xd1000facu, 0xd1016facu}) { + dispatch(rawStore, raw(Output.data(), sizeof(Output), word3)); + Require(all(Fill), "raw store through V# word 3 " + Hex(word3) + " wrote memory"); + dispatch(formattedStore, strided(Output.data(), word3)); + Require(all(Fill), "formatted store through V# word 3 " + Hex(word3) + " wrote memory"); + } + dispatch(formattedStore, {0x02100b00u, 0xc3c00000u, 0x010dc1dfu, 0x91b00facu}); + Require(all(Fill), "store through an image T# read as a V# wrote memory"); + const auto rejects = [&](std::span code, std::uint32_t word3, const char* access) { + bool rejected = false; + try { + dispatch(code, raw(Output.data(), sizeof(Output), word3)); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("buffer descriptor uses an unsupported type") != std::string::npos, std::string(access) + " through V# word 3 " + Hex(word3) + " failed for another reason: " + error.what()); + rejected = true; + } + Require(rejected, std::string(access) + " accepted V# word 3 " + Hex(word3)); + }; + for (const auto word3 : {0x71000facu, 0x71016facu, 0xb107ffacu, 0xf107ffacu}) { + rejects(Code, word3, "a raw load"); + rejects(rawStore, word3, "a raw store"); + rejects(formattedStore, word3, "a formatted store"); + } + for (const auto word3 : {0x91000facu, 0x91016facu, 0xd1000facu, 0xd1016facu}) { + rejects(Code, word3, "a raw load"); + rejects(formattedLoad, word3, "a formatted load"); + rejects(atomicAdd, word3, "an atomic"); + } +} + } int main() { @@ -187,6 +264,7 @@ int main() { Run(*device, false); Run(*device, true); ScalarReads(*device); + DisabledDescriptors(*device); std::puts("buffer descriptor runtime tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index f7d85442af..cb7db83ee4 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -1613,9 +1613,32 @@ std::uint32_t emulatedCompareState(const ShaderInfo& info, const ResourceSnapsho return *samplerState | (reference << EmulatedCompare::ReferenceShift) | (singleLevel ? EmulatedCompare::SingleLevel : 0u); } +bool disabledBufferDescriptor(const BufferResource& buffer, const DescriptorValue& descriptor) { + const auto decoded = decodeBufferDescriptor(descriptor); + const auto format = decoded.RawFormat(); + if (decoded.Type() == 0u) return format == 0u; + return (decoded.Type() == 2u || decoded.Type() == 3u) && format <= static_cast(IrBufferFormat::Format32_32_32_32Float) && !buffer.read; +} + +bool disabledBufferAccesses(const IrResourcePlan& plan, std::uint32_t index, const DescriptorValue& descriptor) { + const auto& buffer = plan.info.buffers[index]; + if (GcnGuestShaders() || buffer.atomic || buffer.scalar || buffer.imageAlias != BufferResource::NoImageAlias || !disabledBufferDescriptor(buffer, descriptor)) return false; + bool accessed = false; + for (const auto& memory : plan.memoryInfo) { + if (memory.kind != ResourceKind::Buffer || memory.resource != index) continue; + if (memory.typed || memory.gpuDescriptor) return false; + accessed = true; + } + return accessed; +} + void materializeTables(const IrResourcePlan& plan, ResourceSnapshot& snapshot) { for (std::uint32_t i = 0u; i < plan.info.buffers.size(); ++i) { const auto& buffer = plan.info.buffers[i]; + if (disabledBufferAccesses(plan, i, snapshot.buffers.at(i))) { + auto& words = snapshot.buffers.at(i).dwords; + std::fill(words.begin(), words.begin() + 4, 0u); + } const bool scalarOnly = buffer.scalar && !buffer.written && !buffer.atomic && !buffer.formatted && buffer.imageAlias == BufferResource::NoImageAlias && std::none_of(plan.memoryInfo.begin(), plan.memoryInfo.end(), [i](const MemoryInfo& memory) { return memory.kind == ResourceKind::Buffer && memory.resource == i; }); if (scalarOnly) { diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index d9a9964ce3..9d270dc76d 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -31,6 +31,7 @@ ### Functional +- [Buffer descriptor type](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) - a buffer V# whose TYPE (word 3 bits 30-31) is not 0 throws `buffer descriptor uses an unsupported type`, except for vector stores through TYPE 2 or 3. Measured on a PS5 (CFI-1115B, firmware 12.20): through TYPE 2 or 3 with FORMAT 0 or a valid format, `buffer_store_dword` and `buffer_store_format_x` wrote nothing and did not fault, also with an unmapped base; loads and GLC atomics left the destination VGPR unchanged, which a null V# can't reproduce, so they throw. TYPE 0 with FORMAT 0 returned 0 for loads and wrote nothing. TYPE 1 faulted the GPU for every access and throws. Unmeasured and still throwing for TYPE 2 and 3: `tbuffer_*`, scalar loads, FORMAT above 77, GCN shaders and V#s loaded at run time; multi-dword stores are assumed to match the one-dword ones. Closes when: those cases are measured on a PS5. - [Pixel interpolators past `SPI_PS_IN_CONTROL.NUM_INTERP`](../../../core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp) (shader recompiler) - when the IR reads an attribute at or past the guest interpolator count, the missing `SPI_PS_INPUT_CNTL` slots are filled as DEFAULT (`OFFSET` 0x20, constant 0/1) before info collection, and SPIR-V emission of `GetInterpolationParameter` for a missing binding returns the same defaults instead of throwing, matching Mesa's treatment of a PS input with no VS output. Component and mode bounds still throw. A retail title prepares a pixel shader with `NUM_INTERP` 0 while it still issues `v_interp` on attribute 0 (`undefined attribute` after the collector fill alone). Console behaviour for that pairing is not measured. - [Windows cache housekeeping](../../../core/shader/recompiler/ShaderDiskCache.cpp): eviction of old versions and temporary files is skipped for `\\?\` and `\\.\` cache roots because the supported WinLibs directory iterator can enumerate the process working directory for long prefixed paths. The active version's `last-used` stamp update still runs; it fails for long `\\.\` aliases with the supported toolchain. Ordinary Windows directory enumeration remains subject to MAX_PATH. From 123f7f3b605fe8468c874e034f6a64d4354669ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0?= Date: Wed, 28 Oct 2026 14:42:32 +0100 Subject: [PATCH 121/160] fix(shader): keep multi-dword TYPE 2/3 stores throwing and move the failed-capture fixture to TYPE 1 --- .../tests/execution/BufferDescriptorRuntime.cpp | 5 ----- .../libSceAgcDriver/tests/execution/FailedCaptureDump.cpp | 4 ++-- .../recompiler/Optimization/src/ResourceMaterializer.cpp | 2 ++ docs/dev/debt/shader.md | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp index 0f8ea4f99b..15aa7c77a2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp @@ -247,11 +247,6 @@ void DisabledDescriptors(AgcDriver::VulkanDevice& device) { rejects(rawStore, word3, "a raw store"); rejects(formattedStore, word3, "a formatted store"); } - for (const auto word3 : {0x91000facu, 0x91016facu, 0xd1000facu, 0xd1016facu}) { - rejects(Code, word3, "a raw load"); - rejects(formattedLoad, word3, "a formatted load"); - rejects(atomicAdd, word3, "an atomic"); - } } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FailedCaptureDump.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FailedCaptureDump.cpp index 0382270371..8697d8b1ba 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/FailedCaptureDump.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FailedCaptureDump.cpp @@ -88,7 +88,7 @@ void Run() { const std::string first = std::string(prefix) + ".req"; const std::string failed = std::string(prefix) + "_failed.req"; Valid = BufferDescriptor(0x00014204u); - Unsupported = BufferDescriptor(0xa1b00facu); + Unsupported = BufferDescriptor(0x61b00facu); Output.fill(Untouched); const auto valid = Pointer(Valid.data()); Dispatch(valid); @@ -101,7 +101,7 @@ void Run() { } catch (const std::exception& error) { message = error.what(); } - Require(message.find("unsupported type") != std::string::npos, "the dispatch through a type 2 descriptor failed for another reason: " + message); + Require(message.find("unsupported type") != std::string::npos, "the dispatch through a type 1 descriptor failed for another reason: " + message); Require(message.find("(" + failed + ")") != std::string::npos, "the failure does not name the failing dispatch's request: " + message); const auto firstText = Read(first); const auto failedText = Read(failed); diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index cb7db83ee4..6944079956 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -1623,10 +1623,12 @@ bool disabledBufferDescriptor(const BufferResource& buffer, const DescriptorValu bool disabledBufferAccesses(const IrResourcePlan& plan, std::uint32_t index, const DescriptorValue& descriptor) { const auto& buffer = plan.info.buffers[index]; if (GcnGuestShaders() || buffer.atomic || buffer.scalar || buffer.imageAlias != BufferResource::NoImageAlias || !disabledBufferDescriptor(buffer, descriptor)) return false; + const bool measuredOnly = decodeBufferDescriptor(descriptor).Type() != 0u; bool accessed = false; for (const auto& memory : plan.memoryInfo) { if (memory.kind != ResourceKind::Buffer || memory.resource != index) continue; if (memory.typed || memory.gpuDescriptor) return false; + if (measuredOnly && (memory.dataDwords != 1u || memory.componentCount != 1u)) return false; accessed = true; } return accessed; diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 9d270dc76d..674d43958d 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -31,7 +31,7 @@ ### Functional -- [Buffer descriptor type](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) - a buffer V# whose TYPE (word 3 bits 30-31) is not 0 throws `buffer descriptor uses an unsupported type`, except for vector stores through TYPE 2 or 3. Measured on a PS5 (CFI-1115B, firmware 12.20): through TYPE 2 or 3 with FORMAT 0 or a valid format, `buffer_store_dword` and `buffer_store_format_x` wrote nothing and did not fault, also with an unmapped base; loads and GLC atomics left the destination VGPR unchanged, which a null V# can't reproduce, so they throw. TYPE 0 with FORMAT 0 returned 0 for loads and wrote nothing. TYPE 1 faulted the GPU for every access and throws. Unmeasured and still throwing for TYPE 2 and 3: `tbuffer_*`, scalar loads, FORMAT above 77, GCN shaders and V#s loaded at run time; multi-dword stores are assumed to match the one-dword ones. Closes when: those cases are measured on a PS5. +- [Buffer descriptor type](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) - a buffer V# whose TYPE (word 3 bits 30-31) is not 0 throws `buffer descriptor uses an unsupported type`, except for vector stores through TYPE 2 or 3. Measured on a PS5 (CFI-1115B, firmware 12.20): through TYPE 2 or 3 with FORMAT 0 or a valid format, `buffer_store_dword` and `buffer_store_format_x` wrote nothing and did not fault, also with an unmapped base; loads and GLC atomics left the destination VGPR unchanged, which a null V# can't reproduce, so they throw. TYPE 0 with FORMAT 0 returned 0 for loads and wrote nothing. TYPE 1 faulted the GPU for every access and throws. Unmeasured and still throwing for TYPE 2 and 3: `tbuffer_*`, scalar loads, FORMAT above 77, GCN shaders and V#s loaded at run time; multi-dword and multi-component stores through TYPE 2 and 3 are unmeasured and throw. Closes when: those cases are measured on a PS5. - [Pixel interpolators past `SPI_PS_IN_CONTROL.NUM_INTERP`](../../../core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp) (shader recompiler) - when the IR reads an attribute at or past the guest interpolator count, the missing `SPI_PS_INPUT_CNTL` slots are filled as DEFAULT (`OFFSET` 0x20, constant 0/1) before info collection, and SPIR-V emission of `GetInterpolationParameter` for a missing binding returns the same defaults instead of throwing, matching Mesa's treatment of a PS input with no VS output. Component and mode bounds still throw. A retail title prepares a pixel shader with `NUM_INTERP` 0 while it still issues `v_interp` on attribute 0 (`undefined attribute` after the collector fill alone). Console behaviour for that pairing is not measured. - [Windows cache housekeeping](../../../core/shader/recompiler/ShaderDiskCache.cpp): eviction of old versions and temporary files is skipped for `\\?\` and `\\.\` cache roots because the supported WinLibs directory iterator can enumerate the process working directory for long prefixed paths. The active version's `last-used` stamp update still runs; it fails for long `\\.\` aliases with the supported toolchain. Ordinary Windows directory enumeration remains subject to MAX_PATH. From 692cc3e025af870ed75999dac3c211ed8815fcee Mon Sep 17 00:00:00 2001 From: simnJS Date: Wed, 28 Oct 2026 14:47:01 +0100 Subject: [PATCH 122/160] test(libSceUsbd): race the cancel on an interrupt endpoint the host can read The device test raced sceUsbdCancelTransfer on the first interrupt IN endpoint of the device. On Windows the first one can belong to a mouse or keyboard collection, which the system opens exclusively, so libusb's HID backend cannot read it and the submission failed, reported as a failed cancel race. The test now tries each interrupt IN endpoint, requires a refusal to be the host's I/O error with nothing in flight, and races on the first one the host accepts, with a buffer sized from that endpoint. --- core/libs/tests/GuestUsbd.cpp | 43 +++++++++++++++++++++++++++++++---- docs/dev/debt/libSceUsbd.md | 1 + 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/core/libs/tests/GuestUsbd.cpp b/core/libs/tests/GuestUsbd.cpp index 0e8f5ecee6..316fcc1319 100644 --- a/core/libs/tests/GuestUsbd.cpp +++ b/core/libs/tests/GuestUsbd.cpp @@ -11,6 +11,7 @@ #include #include #include +#include namespace { @@ -18,12 +19,18 @@ namespace { namespace { +constexpr std::int32_t ioError = static_cast(0x80240001); constexpr std::int32_t invalidArgument = static_cast(0x80240002); constexpr std::int32_t noDevice = static_cast(0x80240004); constexpr std::int32_t notFound = static_cast(0x80240005); constexpr std::int32_t otherError = static_cast(0x802400FF); constexpr std::uint8_t absentEndpoint = 0x70; +struct InterruptEndpoint { + std::uint8_t address; + std::int32_t length; +}; + void Require(bool condition, const char* message) { if (!condition) { std::fprintf(stderr, "USBD: %s\n", message); @@ -141,10 +148,11 @@ void Device() { std::uint8_t interruptAddress = 0; std::uint16_t interruptMax = 0; std::uint8_t interruptInterface = 0; + std::vector interruptEndpoints; if (interfaceArray == nullptr) { Require(false, "the active config descriptor has no interface array"); } - for (std::uint8_t index = 0; index < interfaces && interruptAddress == 0; ++index) { + for (std::uint8_t index = 0; index < interfaces; ++index) { const std::uint8_t* alternate = *reinterpret_cast(interfaceArray + index * 16); if (alternate == nullptr) continue; const std::uint8_t* endpoints = *reinterpret_cast(alternate + 16); @@ -152,8 +160,11 @@ void Device() { for (std::uint8_t endpoint = 0; endpoint < alternate[4]; ++endpoint) { const std::uint8_t* item = endpoints + endpoint * 32; if ((item[2] & 0x80) != 0 && (item[3] & 3) == 3) { + const auto maxPacket = static_cast(item[4] | item[5] << 8); + interruptEndpoints.push_back({item[2], (maxPacket & 0x7FF) * (1 + ((maxPacket >> 11) & 3))}); + if (interruptAddress != 0) continue; interruptAddress = item[2]; - interruptMax = static_cast(item[4] | item[5] << 8); + interruptMax = maxPacket; interruptInterface = alternate[2]; } } @@ -193,13 +204,35 @@ void Device() { Pump(done); Require(done == 1 && transfer->status == 0 && transfer->actualLength == 18, "asynchronous control transfer did not complete"); Require(std::memcmp(setup + 8, descriptor, 18) == 0, "asynchronous device descriptor differs from the cached one"); + const UsbdTimeval slice{0, 200}; + std::vector blink; + const InterruptEndpoint* raced = nullptr; + for (const auto& endpoint : interruptEndpoints) { + std::atomic notified{false}; + blink.assign(static_cast(endpoint.length), 0); + sceUsbdFillInterruptTransfer(reinterpret_cast(transfer), handle, endpoint.address, blink.data(), endpoint.length, &MarkDoneThenSignal, ¬ified, 0); + const std::int32_t submitted = sceUsbdSubmitTransfer(reinterpret_cast(transfer)); + if (submitted != 0) { + std::printf("interrupt endpoint 0x%02x refused a transfer: %d\n", endpoint.address, submitted); + Require(submitted == ioError, "a refused interrupt transfer did not report the host's I/O error"); + Require(sceUsbdCancelTransfer(reinterpret_cast(transfer)) == notFound, "a refused interrupt transfer stayed in flight"); + continue; + } + const std::int32_t cancelled = sceUsbdCancelTransfer(reinterpret_cast(transfer)); + Require(cancelled == 0 || cancelled == notFound, "cancelling an in-flight interrupt transfer failed"); + while (!notified.load(std::memory_order_acquire)) Require(sceUsbdHandleEventsTimeout(&slice) == 0, "event handling failed while an interrupt transfer was cancelled"); + Require(transfer->status == 0 || transfer->status == 3, "a cancelled interrupt transfer neither completed nor reported the cancellation"); + raced = &endpoint; + break; + } + Require(raced != nullptr, "no interrupt IN endpoint accepted a transfer"); + std::printf("cancel race on interrupt endpoint 0x%02x\n", raced->address); bool broken = false; { - const UsbdTimeval slice{0, 200}; for (int round = 0; round < 300 && !broken; ++round) { std::atomic notified{false}; - std::uint8_t blink[64] = {}; - sceUsbdFillInterruptTransfer(reinterpret_cast(transfer), handle, interruptAddress, blink, interruptMax, &MarkDoneThenSignal, ¬ified, 0); + blink.assign(static_cast(raced->length), 0); + sceUsbdFillInterruptTransfer(reinterpret_cast(transfer), handle, raced->address, blink.data(), raced->length, &MarkDoneThenSignal, ¬ified, 0); const int submitResult = sceUsbdSubmitTransfer(reinterpret_cast(transfer)); if (submitResult != 0) { diff --git a/docs/dev/debt/libSceUsbd.md b/docs/dev/debt/libSceUsbd.md index e7e535f68b..a2f39c3804 100644 --- a/docs/dev/debt/libSceUsbd.md +++ b/docs/dev/debt/libSceUsbd.md @@ -11,5 +11,6 @@ ### Functional - [sceUsbdGetConfiguration, sceUsbdGetConfigDescriptorByValue, sceUsbdGetDescriptor, sceUsbdGetStringDescriptorAscii](../../../core/libs/prx/libSceUsbd/Export.cpp) (libSceUsbd) - not measured on a console; they follow FreeBSD 11.4 ([libusb10_desc.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10_desc.c), [libusb20.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb20.c)). The configuration and descriptor queries return host libusb errors. `sceUsbdGetStringDescriptorAscii` rebuilds `libusb20_dev_req_string_simple_sync` with only its 255-byte read: libusb's Windows HID backend reports the requested length as `bLength`, cutting a 4-byte header read to one character. Unlike the host `libusb_get_string_descriptor_ascii` it accepts index 0, drops non-printable characters and `<>` instead of writing `?`, and fails with `0x802400FF`. Closes when: measured on a PS5 +- [sceUsbdSubmitTransfer](../../../core/libs/prx/libSceUsbd/Export.cpp) (libSceUsbd) - on Windows, a transfer on an endpoint of a mouse or keyboard HID collection fails with `0x80240001`, the host's I/O error, and nothing is left in flight. Windows opens these collections for its [exclusive use](https://learn.microsoft.com/en-us/windows-hardware/drivers/hid/top-level-collections-opened-by-windows-for-system-use), so libusb's HID backend [opens them without read or write access](https://github.com/libusb/libusb-cmake/blob/bea6567b63796ab27c1c33257d230284fb2d8316/libusb/libusb/os/windows_winusb.c#L4643-L4653) and [fails](https://github.com/libusb/libusb-cmake/blob/bea6567b63796ab27c1c33257d230284fb2d8316/libusb/libusb/os/windows_winusb.c#L5008-L5012) the refused `ReadFile`. What the console answers for a keyboard or mouse its own kernel drives is unknown - [sceUsbdGetDevice, sceUsbdGetDeviceSpeed, sceUsbdGetMaxPacketSize, sceUsbdGetMaxIsoPacketSize, sceUsbdOpenDeviceWithVidPid](../../../core/libs/prx/libSceUsbd/Export.cpp) (libSceUsbd) - not measured on a console; they follow FreeBSD 11.4 [libusb10.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10.c) over the host libusb. The packet sizes are searched in the active configuration and the iso size is scaled as FreeBSD does, not through the host's versions, which differ for an unreadable configuration and for SuperSpeed endpoints. A host SuperSpeed+ device reports SuperSpeed, the fastest speed FreeBSD 11.4 has. A null device in `sceUsbdGetMaxIsoPacketSize` throws, as FreeBSD dereferences it. A device stays known while a handle to it is open, so the device of a handle from `sceUsbdOpenDeviceWithVidPid` can be used until that handle is closed. Closes when: measured on a PS5 - [sceUsbdSetInterfaceAltSetting, sceUsbdClearHalt, sceUsbdDetachKernelDriver](../../../core/libs/prx/libSceUsbd/Export.cpp) (libSceUsbd) - not measured on a console; they follow FreeBSD 11.4 [libusb10.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10.c) over the host libusb. `sceUsbdSetInterfaceAltSetting` checks the interface range and this library's record of claimed interfaces, and returns `0x802400FF` for any other failure; it throws while the device has transfers in flight, which FreeBSD cancels first. `sceUsbdClearHalt` returns 0, or `0x802400FF` for an endpoint the host does not find; FreeBSD ignores the result of the clear itself, so the host's other errors throw, and so does a null handle, which FreeBSD dereferences. `sceUsbdDetachKernelDriver` returns `0x802400FF` for any failure, which on Windows is every call, as libusb cannot detach drivers there. Closes when: measured on a PS5 From 161606cf61b4cfab1c632c1785790ccadbec402d Mon Sep 17 00:00:00 2001 From: StoneDC <19905433+StoneDC@users.noreply.github.com> Date: Wed, 28 Oct 2026 21:15:09 +0800 Subject: [PATCH 123/160] fix(web): validate library ZIP compatibility before writing --- demo/web/archive.mjs | 3 +++ demo/web/tests/archive.test.mjs | 22 ++++++++++++++++++++++ tools/build_web.py | 4 ++++ tools/tests/test_build_web.py | 25 +++++++++++++++++++++++++ 4 files changed, 54 insertions(+) diff --git a/demo/web/archive.mjs b/demo/web/archive.mjs index 7021114f0e..a2d12e63dc 100644 --- a/demo/web/archive.mjs +++ b/demo/web/archive.mjs @@ -346,6 +346,9 @@ export async function packageArchive(archive, converted, outputName, libraryFile if (!libraryEntries.length || libraryEntries.length > 4096) throw new Error("Invalid library archive"); for (const entry of libraryEntries) { if (!entry.filename.startsWith("libs/") || entry.encrypted || ((entry.externalFileAttributes >>> 16) & 0xf000) === 0xa000) throw new Error("Invalid library archive layout"); + if (entry.compressionMethod !== 0 && entry.compressionMethod !== 8) throw new Error(`Unsupported ZIP compression: ${entry.filename}`); + const mode = (entry.externalFileAttributes >>> 16) & 0xf000; + if (mode && mode !== 0x8000 && mode !== 0x4000) throw new Error(`Links and special files are not supported: ${entry.filename}`); claim(entry.filename); } const replaced = new Set(converted.modules.map(module => module.name)); diff --git a/demo/web/tests/archive.test.mjs b/demo/web/tests/archive.test.mjs index d0c2ca7c77..1815455595 100644 --- a/demo/web/tests/archive.test.mjs +++ b/demo/web/tests/archive.test.mjs @@ -275,3 +275,25 @@ delete globalThis.self; console.log("Rate-limited release checks use the verified site manifest; other release-check failures remain errors"); console.log("Windows folder output rejected before conversion or file writes"); console.log("Multiple ZIP inputs stream into separate app roots; invalid inputs name the failing archive"); + +for (const compression of [12, 14]) { + const stored = await zip([["libs/system.prx", new Uint8Array([1, 2, 3]), { level: 0 }]]); + const bytes = new Uint8Array(await stored.arrayBuffer()); + const view = new DataView(bytes.buffer); + for (let offset = 0; offset + 46 <= bytes.length; offset++) { + const signature = view.getUint32(offset, true); + if (signature === 0x04034b50) view.setUint16(offset + 8, compression, true); + if (signature === 0x02014b50) view.setUint16(offset + 10, compression, true); + } + let writes = 0; + await assert.rejects(packageArchive({ files: [] }, { executable: new Uint8Array([1]), modules: [] }, "program.elf", + new Blob([bytes]), { async file() { writes++; throw new Error("destination opened"); } }, () => {}, true), /Unsupported ZIP compression/u); + assert.equal(writes, 0); +} +for (const mode of [0o010644, 0o020644, 0o060644, 0o140644]) { + const special = await zip([["libs/special", new Uint8Array([1]), { versionMadeBy: 0x31e, externalFileAttributes: (mode << 16) >>> 0 }]]); + let writes = 0; + await assert.rejects(packageArchive({ files: [] }, { executable: new Uint8Array([1]), modules: [] }, "program.elf", + special, { async file() { writes++; throw new Error("destination opened"); } }, () => {}, true), /special files/u); + assert.equal(writes, 0); +} diff --git a/tools/build_web.py b/tools/build_web.py index 104161f497..213899b0bb 100644 --- a/tools/build_web.py +++ b/tools/build_web.py @@ -104,6 +104,10 @@ def publish_libraries(output, release=None): raise ValueError("Invalid library archive") if any(file.flag_bits & 1 for file in files): raise ValueError(f"Encrypted release archive {asset['name']} is not supported") + if any(file.compress_type not in (zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED) for file in files): + raise ValueError("Library ZIP archive compression must be Store or Deflate") + if any((file.external_attr >> 16) & 0o170000 not in (0, 0o100000, 0o040000) for file in files): + raise ValueError("Library ZIP links and special files are not supported") names = {} for file in files: if (file.orig_filename != file.filename or not file.filename.startswith("libs/") diff --git a/tools/tests/test_build_web.py b/tools/tests/test_build_web.py index 3c0506b29e..9e145e25ed 100644 --- a/tools/tests/test_build_web.py +++ b/tools/tests/test_build_web.py @@ -183,6 +183,31 @@ class LibraryPublicationTests(unittest.TestCase): self.assertFalse(any(file.suffix == ".download" for file in self.libraries.iterdir())) + def test_browser_incompatible_compression_and_special_files_preserve_old_release(self): + valid = library_zip([("libs/libc.prx", b"valid")]) + for compression in (zipfile.ZIP_BZIP2, zipfile.ZIP_LZMA): + data = io.BytesIO() + with zipfile.ZipFile(data, "w", compression=compression) as archive: + archive.writestr("libs/libc.prx", b"content") + with self.subTest(compression=compression), self.assertRaisesRegex(ValueError, "compression"): + self.publish(data.getvalue(), valid) + self.assertEqual(self.manifest.read_bytes(), self.previous) + self.assertEqual(list(self.libraries.iterdir()), [self.existing]) + for mode in (0o010644, 0o020644, 0o060644, 0o120777, 0o140644): + entry = zipfile.ZipInfo("libs/special") + entry.create_system = 3 + entry.external_attr = mode << 16 + with self.subTest(mode=mode), self.assertRaisesRegex(ValueError, "special files"): + self.publish(library_zip([(entry, b"content")]), valid) + self.assertEqual(self.manifest.read_bytes(), self.previous) + for compression in (zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED): + data = io.BytesIO() + with zipfile.ZipFile(data, "w", compression=compression) as archive: + archive.writestr("libs/libc.prx", b"valid") + self.publish(data.getvalue(), data.getvalue()) + self.assertEqual(json.loads(self.manifest.read_text(encoding="utf-8"))["tag"], "v1") + + class BuildWebCliTests(unittest.TestCase): def test_missing_relinker_wasm_reports_error_without_traceback(self): with tempfile.TemporaryDirectory() as directory: From 5ba5b9273c31c304c9e1906a6125457936112765 Mon Sep 17 00:00:00 2001 From: StoneDC <19905433+StoneDC@users.noreply.github.com> Date: Wed, 28 Oct 2026 21:15:12 +0800 Subject: [PATCH 124/160] fix(web): serialize output directory transaction cleanup --- demo/web/output-directory.mjs | 14 +++++--- demo/web/tests/output-directory.test.mjs | 44 ++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/demo/web/output-directory.mjs b/demo/web/output-directory.mjs index ee199e1341..958f81f5f8 100644 --- a/demo/web/output-directory.mjs +++ b/demo/web/output-directory.mjs @@ -1,5 +1,6 @@ export class OutputDirectoryTransaction { #state = "open"; + #discard; constructor(parent, name, directory) { this.parent = parent; @@ -23,10 +24,15 @@ export class OutputDirectoryTransaction { this.#state = "committed"; } - async discard() { - if (this.#state !== "open") return; - await this.parent.removeEntry(this.name, { recursive: true }); - this.#state = "discarded"; + discard() { + if (this.#state === "discarding") return this.#discard; + if (this.#state !== "open") return Promise.resolve(); + this.#state = "discarding"; + this.#discard = Promise.resolve().then(() => this.parent.removeEntry(this.name, { recursive: true })).then( + () => { this.#state = "discarded"; }, + error => { this.#state = "open"; throw error; } + ); + return this.#discard; } } diff --git a/demo/web/tests/output-directory.test.mjs b/demo/web/tests/output-directory.test.mjs index 873572a738..68a6fff633 100644 --- a/demo/web/tests/output-directory.test.mjs +++ b/demo/web/tests/output-directory.test.mjs @@ -114,3 +114,47 @@ test("returns sink and folder cleanup failures without hiding either", async () "Could not remove incomplete output folder partial-linux/: directory permission denied" ]); }); + +test("concurrent cleanup shares one removal and prevents commit until it settles", { timeout: 3000 }, async () => { + let finish; + let calls = 0; + const parent = { removeEntry() { calls++; return new Promise(resolve => { finish = resolve; }); } }; + const transaction = new OutputDirectoryTransaction(parent, "partial", {}); + const first = transaction.discard(); + const second = transaction.discard(); + assert.equal(first, second); + assert.throws(() => transaction.commit(), /no longer active/u); + await Promise.resolve(); + assert.equal(calls, 1); + finish(); + await Promise.all([first, second]); + await transaction.discard(); + assert.equal(calls, 1); + assert.throws(() => transaction.commit(), /no longer active/u); +}); + +test("concurrent cleanup observes the same rejection and can retry", { timeout: 3000 }, async () => { + let fail; + let calls = 0; + const parent = { removeEntry() { + calls++; + return calls === 1 ? new Promise((resolve, reject) => { fail = reject; }) : Promise.resolve(); + } }; + const transaction = new OutputDirectoryTransaction(parent, "partial", {}); + const first = transaction.discard(); + const second = transaction.discard(); + const results = Promise.allSettled([first, second]); + await Promise.resolve(); + const error = new Error("busy"); + fail(error); + assert.deepEqual(await results, [{ status: "rejected", reason: error }, { status: "rejected", reason: error }]); + await transaction.discard(); + assert.equal(calls, 2); +}); + +test("synchronous removal failure restores an active transaction", async () => { + const transaction = new OutputDirectoryTransaction({ removeEntry() { throw new Error("busy"); } }, "partial", {}); + await assert.rejects(transaction.discard(), /busy/u); + transaction.commit(); + await transaction.discard(); +}); From ae52db874de5f0ce06f38a362d56fadbbe9f91a5 Mon Sep 17 00:00:00 2001 From: StoneDC <19905433+StoneDC@users.noreply.github.com> Date: Wed, 28 Oct 2026 21:15:15 +0800 Subject: [PATCH 125/160] fix(tools): clamp release ZIP timestamps to their valid range --- tools/package_release.py | 4 ++-- tools/tests/test_package_release.py | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/tools/package_release.py b/tools/package_release.py index 2cafffac3b..bb261f549c 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -85,7 +85,7 @@ def package(platform, build, output, version): output.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(prefix=".release-", dir=output) as temporary: staging = Path(temporary) - with zipfile.ZipFile(staging / f"prx-{platform}-{version}.zip", "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive: + with zipfile.ZipFile(staging / f"prx-{platform}-{version}.zip", "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9, strict_timestamps=False) as archive: for file in files: archive.write(file, arcname=f"libs/{file.name}") for source, destination in licenses: @@ -137,7 +137,7 @@ def package_web(build, output, version, revision): output.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(prefix=".release-", dir=output) as temporary: asset = Path(temporary) / name - with zipfile.ZipFile(asset, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive: + with zipfile.ZipFile(asset, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9, strict_timestamps=False) as archive: for file in WEB_FILES: archive.write(source / file, arcname=file) archive.writestr("web-release.json", json.dumps({"tag": version, "revision": revision}) + "\n") diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index 4216bc439c..1851c33507 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -390,6 +390,29 @@ class ReleasePackageTests(unittest.TestCase): package_release.windows_runtime(Path(tmp)) + def test_native_and_web_zip_timestamps_are_clamped_without_changing_files(self): + import os + for timestamp, year in ((0, 1980), (7258118400, 2107)): + with self.subTest(timestamp=timestamp): + web = self.build / "web" + web.mkdir(exist_ok=True) + for name in package_release.WEB_FILES: + (web / name).parent.mkdir(parents=True, exist_ok=True) + (web / name).write_bytes(b"web fixture") + os.utime(web / name, (timestamp, timestamp)) + os.utime(self.library, (timestamp, timestamp)) + before = self.library.stat().st_mtime_ns + package_release.package("linux", self.build, self.output, "v1") + package_release.package_web(self.build, self.output, "v1", "a" * 40) + for asset, member, contents in (("prx-linux-v1.zip", "libs/libc.prx", b"library bytes"), + ("web-v1.zip", package_release.WEB_FILES[0], b"web fixture")): + with zipfile.ZipFile(self.output / asset) as archive: + self.assertEqual(archive.getinfo(member).date_time[0], year) + self.assertEqual(archive.read(member), contents) + self.assertEqual(self.library.stat().st_mtime_ns, before) + self.assertEqual((web / package_release.WEB_FILES[0]).stat().st_mtime, timestamp) + + class DocumentationAssetsTests(unittest.TestCase): def test_source_copy_failure_does_not_publish_partial_documents(self): From 7db5b84dce999b6c8414a994c24d107dd5a0acbf Mon Sep 17 00:00:00 2001 From: Kyon Date: Wed, 28 Oct 2026 13:53:47 +0000 Subject: [PATCH 126/160] fix(libSceBgft): clarify export identities and verifiable membership --- docs/dev/debt/libSceBgft.md | 38 ++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/docs/dev/debt/libSceBgft.md b/docs/dev/debt/libSceBgft.md index 2c75eea670..f8e91c27e7 100644 --- a/docs/dev/debt/libSceBgft.md +++ b/docs/dev/debt/libSceBgft.md @@ -18,19 +18,27 @@ ### Additional callable export inventory -- [Additional callable exports](../../../core/libs/prx/libSceBgft/Export.cpp) - 12 entries have import-resolution declarations only. Parameter lists, return types and behavior are unverified; parameterless `int` declarations are placeholders, not verified ABI contracts. Every entry throws through `NotImplemented_nid_no_patch`. Unidentified names use literal NID aliases. No runtime support, implemented-function increase or title unblock is claimed. +- [Additional exports](../../../core/libs/prx/libSceBgft/Export.cpp) - 12 import-resolution declarations (7 named candidates and 5 unresolved NIDs). All entries call `NotImplemented_nid_no_patch`. Parameterless `int` declarations do not establish parameter or return ABI; no runtime implementation or title unblock is claimed. -| Name | NID | -| --- | --- | -| `sceBgftServiceIntDownloadClearAllInstalled` | `XEJKc+Ivvm4` | -| `sceBgftServiceIntDownloadClearInstalled` | `bmSUGdmdJto` | -| `sceBgftServiceIntDownloadPauseGroupTasks` | `cHMDnBGpQoo` | -| `sceBgftServiceIntDownloadResumeGroupTasks` | `Ys2nTRwMM8M` | -| `sceBgftServiceIntDownloadStartGroupTasks` | `3DeLtzqsmG8` | -| `sceBgftServiceIntDownloadUnregisterCompleteTask` | `TAee3wrD-M0` | -| `sceBgftServiceIntUploadUnregisterCompleteTask` | `I9k+LTS1OlI` | -| `Unknown` | `3cvL6QTobNg` | -| `Unknown` | `4JQKNtsVDpg` | -| `Unknown` | `85axuUkg9eE` | -| `Unknown` | `fJnnIc5sHOU` | -| `Unknown` | `tBOVSvi9EjY` | +#### Membership and identity evidence + +The inspected `PT_SCE_MODULEPARAM` record contains version words `0x13590001` at offset `0x10` and `0x13600007` at offset `0x14`. The 13.60 label refers to the latter SDK-formatted word, not independently authenticated retail firmware or an introduction date. The [reproducible static audit](https://github.com/boykopovar/AnyPS5/pull/12367#issuecomment-6470603298) records the extraction/filtering procedure and the observed NID-set digest. Each row below was read as a defined `STT_FUNC` entry; RVA and size are symbol-table values, not runtime addresses or function bytes. + +Unique function NIDs in the inspected module: **82**. SHA-256 of sorted unique NIDs (one ASCII NID plus LF per row): `1febc3e107d7f24aae994a6d92ab071e4d8aa5ec34b5a4cc70ab8980ee463d10`. All 12 proposed NIDs are members of that set. + +Candidate names marked **aerolib** occur in the [repository-pinned table](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv). Names marked **public candidate** occur in the [public ps5rs identity database at commit `2304be5131221bae0dac24b9767cabffda33f747`](https://github.com/claimore22/ps5rs/blob/2304be5131221bae0dac24b9767cabffda33f747/data/nids.csv). Every named candidate was recomputed with the repository NID algorithm and matched to a function NID in the inspected table; no name was assigned by ordinal or adjacent address. Those databases support spelling, not firmware attribution, prototypes or behavior. Unresolved names remain explicit NID aliases. + +| Candidate / unresolved | NID | Name evidence | ELF RVA | Symbol bytes | +| --- | --- | --- | --- | ---: | +| `sceBgftServiceIntDownloadClearAllInstalled` | `XEJKc+Ivvm4` | public candidate | `0x340` | 18 | +| `sceBgftServiceIntDownloadClearInstalled` | `bmSUGdmdJto` | public candidate | `0x360` | 21 | +| `sceBgftServiceIntDownloadPauseGroupTasks` | `cHMDnBGpQoo` | public candidate | `0x440` | 17 | +| `sceBgftServiceIntDownloadResumeGroupTasks` | `Ys2nTRwMM8M` | public candidate | `0x460` | 17 | +| `sceBgftServiceIntDownloadStartGroupTasks` | `3DeLtzqsmG8` | public candidate | `0x430` | 15 | +| `sceBgftServiceIntDownloadUnregisterCompleteTask` | `TAee3wrD-M0` | public candidate | `0x320` | 14 | +| `sceBgftServiceIntUploadUnregisterCompleteTask` | `I9k+LTS1OlI` | public candidate | `0x640` | 14 | +| `Unknown` | `3cvL6QTobNg` | unresolved | `0x810` | 17 | +| `Unknown` | `4JQKNtsVDpg` | unresolved | `0x830` | 18 | +| `Unknown` | `85axuUkg9eE` | unresolved | `0x480` | 23 | +| `Unknown` | `fJnnIc5sHOU` | unresolved | `0x2f0` | 18 | +| `Unknown` | `tBOVSvi9EjY` | unresolved | `0x530` | 17 | From e359410014b2fdf2dec01dddb4a11516a640f10f Mon Sep 17 00:00:00 2001 From: simnJS Date: Wed, 28 Oct 2026 14:47:30 +0100 Subject: [PATCH 127/160] fix(libSceJxr): reject null and read-only packet state in readIS and writeIS as invalid arguments jxrlib's readIS and writeIS use the bit state, the stream and its callbacks without checks, and pass a null packet to the read or write callback before dereferencing or offsetting it. Throw std::invalid_argument at those points instead of NotImplemented_nid_no_patch, and let a null packet reach the callback first, so a failed write of a null packet returns the callback's error as jxrlib does. --- core/libs/prx/libSceJxr/Export.cpp | 14 +++-- core/libs/tests/JxrStream.cpp | 90 ++++++++++++++++++++++++++---- docs/dev/debt/libSceJxr.md | 2 +- 3 files changed, 89 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceJxr/Export.cpp b/core/libs/prx/libSceJxr/Export.cpp index df9d322a78..904c46b1f8 100644 --- a/core/libs/prx/libSceJxr/Export.cpp +++ b/core/libs/prx/libSceJxr/Export.cpp @@ -25,20 +25,21 @@ std::int64_t APS5_VABI PKFree_nid_postfix(void** memory) { } std::int64_t APS5_VABI readIS_nid_postfix(void*, Jxr::BitIo* io) { - if (!io) NotImplemented_nid_no_patch(__func__); + if (!io) APS5_INVALID_ARG_EX; constexpr std::uintptr_t packetSize = 4096; const auto start = reinterpret_cast(io->start); if (((start ^ reinterpret_cast(io->current)) & packetSize) == 0) return WmpErrSuccess; auto* stream = io->stream; - if (!stream || !stream->setPosition) NotImplemented_nid_no_patch(__func__); + if (!stream || !stream->setPosition) APS5_INVALID_ARG_EX; stream->setPosition(stream, io->referenceOffset); - if (!io->start || !stream->read) NotImplemented_nid_no_patch(__func__); + if (!stream->read) APS5_INVALID_ARG_EX; { GuestArena::HostWrite access(io->start, packetSize); - if (!access.Open()) NotImplemented_nid_no_patch(__func__); + if (!access.Open()) APS5_INVALID_ARG_EX; stream->read(stream, io->start, packetSize); } io->referenceOffset += packetSize; + if (!io->start) APS5_INVALID_ARG_EX; std::memcpy(&io->shadow, io->start, sizeof(io->shadow)); const auto mask = static_cast(static_cast(io->mask)); io->start = reinterpret_cast((reinterpret_cast(io->start) + packetSize) & mask); @@ -46,13 +47,14 @@ std::int64_t APS5_VABI readIS_nid_postfix(void*, Jxr::BitIo* io) { } std::int64_t APS5_VABI writeIS_nid_postfix(void*, Jxr::BitIo* io) { - if (!io) NotImplemented_nid_no_patch(__func__); + if (!io) APS5_INVALID_ARG_EX; constexpr std::uintptr_t packetSize = 4096; const auto start = reinterpret_cast(io->start); if (((start ^ reinterpret_cast(io->current)) & packetSize) == 0) return WmpErrSuccess; - if (!io->start || !io->stream || !io->stream->write) NotImplemented_nid_no_patch(__func__); + if (!io->stream || !io->stream->write) APS5_INVALID_ARG_EX; const auto result = io->stream->write(io->stream, io->start, packetSize); if (result < 0) return result; + if (!io->start) APS5_INVALID_ARG_EX; const auto mask = static_cast(static_cast(io->mask)); io->start = reinterpret_cast((reinterpret_cast(io->start) + packetSize) & mask); return result; diff --git a/core/libs/tests/JxrStream.cpp b/core/libs/tests/JxrStream.cpp index b847bbb8d8..7cf587da0c 100644 --- a/core/libs/tests/JxrStream.cpp +++ b/core/libs/tests/JxrStream.cpp @@ -44,6 +44,16 @@ void Require(bool result, const char* message) { std::abort(); } +template +void RequireInvalidArgument(TAction action, const char* message) { + try { + action(); + } catch (const std::invalid_argument&) { + return; + } + Require(false, message); +} + std::int64_t APS5_VABI Read(Jxr::Stream* stream, void* buffer, std::size_t size) { calls.push_back('r'); lastStream = stream; @@ -167,12 +177,76 @@ void CallbackMutation(Jxr::Stream& stream) { mutation = {}; } -#ifdef _WIN32 -std::int64_t APS5_VABI ObserveRead(Jxr::Stream*, void*, std::size_t) { +std::int64_t APS5_VABI ObserveRead(Jxr::Stream*, void* buffer, std::size_t size) { calls.push_back('r'); + lastBuffer = buffer; + lastSize = size; return 0; } -#endif + +std::int64_t APS5_VABI ObserveWrite(Jxr::Stream*, const void* buffer, std::size_t size) { + calls.push_back('w'); + lastBuffer = const_cast(buffer); + lastSize = size; + return writeResult; +} + +void NullState(Jxr::Stream& stream) { + RequireInvalidArgument([] { readIS_nid_postfix(nullptr, nullptr); }, "read accepted a null bit state"); + RequireInvalidArgument([] { writeIS_nid_postfix(nullptr, nullptr); }, "write accepted a null bit state"); + + auto* packet = ring.data() + PacketSize; + Jxr::Stream partial = stream; + for (auto* selected : {static_cast(nullptr), &partial}) { + partial.setPosition = nullptr; + Jxr::BitIo io{7, 19, 13, ~8192, ring.data(), packet, selected, 91}; + const auto initial = io; + calls.clear(); + RequireInvalidArgument([&] { readIS_nid_postfix(nullptr, &io); }, "read accepted a null stream or seek callback"); + Require(calls.empty() && std::memcmp(&io, &initial, sizeof(io)) == 0, "rejected read seek performed I/O or mutated state"); + partial.write = nullptr; + RequireInvalidArgument([&] { writeIS_nid_postfix(nullptr, &io); }, "write accepted a null stream or write callback"); + Require(calls.empty() && std::memcmp(&io, &initial, sizeof(io)) == 0, "rejected write performed I/O or mutated state"); + partial = stream; + } + + partial.read = nullptr; + seekResult = 0; + Jxr::BitIo io{7, 19, 13, ~8192, ring.data(), packet, &partial, 91}; + auto initial = io; + calls.clear(); + RequireInvalidArgument([&] { readIS_nid_postfix(nullptr, &io); }, "read accepted a null read callback"); + Require(calls == std::vector({'s'}) && lastPosition == 91, "null read callback was not reached after the seek"); + Require(std::memcmp(&io, &initial, sizeof(io)) == 0, "null read callback mutated state"); + + Jxr::Stream observed = stream; + observed.read = ObserveRead; + observed.write = ObserveWrite; + io = {7, 19, 13, ~8192, nullptr, reinterpret_cast(PacketSize), &observed, 91}; + initial = io; + calls.clear(); + lastBuffer = ring.data(); + RequireInvalidArgument([&] { readIS_nid_postfix(nullptr, &io); }, "read accepted a null packet"); + Require(calls == std::vector({'s', 'r'}) && lastBuffer == nullptr && lastSize == PacketSize, "null packet did not reach the read callback first"); + Require(io.referenceOffset == 91 + PacketSize && io.start == nullptr && io.shadow == initial.shadow, "null packet read did not stop at the shadow copy"); + + for (std::int64_t result : {std::int64_t{-73}, std::numeric_limits::min()}) { + io = initial; + writeResult = result; + calls.clear(); + lastBuffer = ring.data(); + Require(writeIS_nid_postfix(nullptr, &io) == result, "failed write of a null packet did not return the callback result"); + Require(calls == std::vector({'w'}) && lastBuffer == nullptr && lastSize == PacketSize, "null packet did not reach the write callback"); + Require(std::memcmp(&io, &initial, sizeof(io)) == 0, "failed write of a null packet mutated state"); + } + for (std::int64_t result : {std::int64_t{0}, std::int64_t{0x100000001}}) { + io = initial; + writeResult = result; + calls.clear(); + RequireInvalidArgument([&] { writeIS_nid_postfix(nullptr, &io); }, "successful write advanced a null packet"); + Require(calls == std::vector({'w'}) && std::memcmp(&io, &initial, sizeof(io)) == 0, "successful write of a null packet mutated state"); + } +} void GuestMapping(Jxr::Stream& stream) { constexpr std::size_t mappingSize = 0x4000; @@ -206,13 +280,8 @@ void GuestMapping(Jxr::Stream& stream) { std::array before; std::copy_n(bytes, mappingSize, before.begin()); calls.clear(); - bool rejected = false; - try { - readIS_nid_postfix(nullptr, &io); - } catch (const std::runtime_error&) { - rejected = true; - } - Require(rejected && calls == std::vector({'s'}), "rejected guest packet write invoked the read callback"); + RequireInvalidArgument([&] { readIS_nid_postfix(nullptr, &io); }, "read-only guest packet backing was not rejected"); + Require(calls == std::vector({'s'}), "rejected guest packet write invoked the read callback"); Require(std::memcmp(&io, &initial, sizeof(io)) == 0 && std::equal(before.begin(), before.end(), bytes), "rejected guest packet write mutated state or backing"); Require(sceKernelMprotect(mapping, mappingSize, 3) == 0, "restore writable guest packet backing"); #endif @@ -231,6 +300,7 @@ int main() { Writes(stream); Buffered(); CallbackMutation(stream); + NullState(stream); GuestMapping(stream); return 0; } diff --git a/docs/dev/debt/libSceJxr.md b/docs/dev/debt/libSceJxr.md index 7e2a8ea81f..e425528f86 100644 --- a/docs/dev/debt/libSceJxr.md +++ b/docs/dev/debt/libSceJxr.md @@ -6,4 +6,4 @@ - [PKFree](../../../core/libs/prx/libSceJxr/Export.cpp) (libSceJxr) follows Microsoft's jxrlib [JXRGlue.c](https://github.com/4creators/jxrlib/blob/f7521879862b9085318e814c6157490dd9dbbdb4/jxrgluelib/JXRGlue.c#L53-L62), whose four exported names the module shares: a non-null `ppv` has `*ppv` released with the guest `free` and set to null, and the result is always `WMP_errSuccess` (0) as a 64-bit `ERR` (`long`). That the console's module is built from jxrlib, and that its `PKFree` releases through libc's `free`, is inferred from the export names and not checked against the console's code. Closes when: the console's `PKFree` is disassembled or measured on a PS5 -- [readIS and writeIS](../../../core/libs/prx/libSceJxr/Export.cpp) use the non-ARM, LP64 `BitIOInfo` and `WMPStream` ABI and packet algorithm from [Microsoft's JPEG XR reference implementation](https://github.com/4creators/jxrlib/blob/f7521879862b9085318e814c6157490dd9dbbdb4/image/sys/strcodec.c#L1090-L1177). Console structure layouts and execution remain unverified. Profiling callbacks are omitted as in the reference Unix build with `DISABLE_PERF_MEASUREMENT`; the codec argument is unused. Invalid null state or required callbacks throw instead of dereferencing them. No codec is instantiated by these helpers; callers must supply the reference ring buffer and guest-ABI stream callbacks. +- [readIS and writeIS](../../../core/libs/prx/libSceJxr/Export.cpp) use the non-ARM, LP64 `BitIOInfo` and `WMPStream` ABI and packet algorithm from [Microsoft's JPEG XR reference implementation](https://github.com/4creators/jxrlib/blob/f7521879862b9085318e814c6157490dd9dbbdb4/image/sys/strcodec.c#L1090-L1177). Console structure layouts and execution remain unverified. Profiling callbacks are omitted as in the reference Unix build with `DISABLE_PERF_MEASUREMENT`; the codec argument is unused. Where the reference uses an invalid pointer, `std::invalid_argument` is thrown instead: a null bit state, stream or callback, a read-only read packet, and a null packet when a read copies its shadow or a successful write offsets it; a null packet still reaches the callback first. No codec is instantiated by these helpers; callers must supply the reference ring buffer and guest-ABI stream callbacks. From 07883249e6dd1d743fa94819893dc8745d351f07 Mon Sep 17 00:00:00 2001 From: simnJS Date: Wed, 28 Oct 2026 14:50:10 +0100 Subject: [PATCH 128/160] fix(libSceIcu): follow ICU for null escape inputs and negative u_unescape capacities ICU 78.1's u_unescapeAt dereferences the offset and calls the callback without checks, and u_unescape reads the source without one, so a null one faults: throw std::invalid_argument there instead of NotImplemented_nid_no_patch. A negative u_unescape capacity is clamped by _appendUChars and fails every room check, so nothing is written and the full length is returned; only a capacity whose signed difference with the output length overflows, which ICU leaves undefined, still throws. --- core/libs/prx/libSceIcu/Unescape.cpp | 15 +++++-- core/libs/prx/libSceIcu/tests/Unescape.cpp | 51 +++++++++++++++++----- docs/dev/debt/libSceIcu.md | 2 +- 3 files changed, 54 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceIcu/Unescape.cpp b/core/libs/prx/libSceIcu/Unescape.cpp index acf0b2abaf..19d68887dc 100644 --- a/core/libs/prx/libSceIcu/Unescape.cpp +++ b/core/libs/prx/libSceIcu/Unescape.cpp @@ -29,7 +29,7 @@ int Digit(char16_t value, int base) { std::int32_t Decode(Icu::UnescapeCharacterAt characterAt, std::int32_t& position, std::int32_t length, void* context) { const auto start = position; if (position < 0 || position >= length) return -1; - if (!characterAt) NotImplemented_nid_no_patch("u_unescapeAt: null callback"); + if (!characterAt) throw std::invalid_argument("u_unescapeAt: null callback"); const auto initial = characterAt(position++, context); bool braces = false; int minimum = 0; @@ -117,17 +117,22 @@ char16_t APS5_VABI ByteAt(std::int32_t position, void* context) { extern "C" { std::int32_t APS5_VABI u_unescapeAt_nid_postfix(Icu::UnescapeCharacterAt characterAt, std::int32_t* offset, std::int32_t length, void* context) { - if (!offset) NotImplemented_nid_no_patch(__func__); + if (!offset) APS5_INVALID_ARG_EX; return Decode(characterAt, *offset, length, context); } std::int32_t APS5_VABI u_unescape_nid_postfix(const char* source, char16_t* destination, std::int32_t capacity) { - if (!source || (destination && capacity < 0)) NotImplemented_nid_no_patch(__func__); + if (!source) APS5_INVALID_ARG_EX; const auto bytes = std::strlen(source); if (bytes > static_cast(std::numeric_limits::max())) NotImplemented_nid_no_patch(__func__); const auto length = static_cast(bytes); + const auto room = [&](std::int32_t used) { + if (destination && static_cast(capacity) - used < std::numeric_limits::min()) + NotImplemented_nid_no_patch("u_unescape: capacity minus the output length overflows"); + }; std::int32_t written = 0; + std::int32_t segment = 0; for (std::int32_t position = 0; position < length;) { const auto byte = static_cast(source[position++]); if (byte != '\\') { @@ -135,11 +140,13 @@ std::int32_t APS5_VABI u_unescape_nid_postfix(const char* source, char16_t* dest ++written; continue; } + if (written != segment) room(segment); const auto value = Decode(ByteAt, position, length, const_cast(source)); if (value < 0) { if (destination && capacity > 0) destination[0] = 0; return 0; } + room(written); const int units = value > 0xffff ? 2 : 1; if (destination && written <= capacity && units <= capacity - written) { if (units == 1) destination[written] = static_cast(value); @@ -149,7 +156,9 @@ std::int32_t APS5_VABI u_unescape_nid_postfix(const char* source, char16_t* dest } } written += units; + segment = written; } + if (written != segment) room(segment); if (destination && written < capacity) destination[written] = 0; return written; } diff --git a/core/libs/prx/libSceIcu/tests/Unescape.cpp b/core/libs/prx/libSceIcu/tests/Unescape.cpp index 1dc7152940..ff9bf07025 100644 --- a/core/libs/prx/libSceIcu/tests/Unescape.cpp +++ b/core/libs/prx/libSceIcu/tests/Unescape.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -22,9 +23,15 @@ void Require(bool condition, std::source_location location = std::source_locatio } template -void RequireThrows(TAction action) { +void RequireThrows(TAction action, std::source_location location = std::source_location::current()) { try { action(); } catch (const std::exception&) { return; } - Require(false); + Require(false, location); +} + +template +void RequireInvalidArgument(TAction action, std::source_location location = std::source_location::current()) { + try { action(); } catch (const std::invalid_argument&) { return; } + Require(false, location); } struct Input { @@ -116,8 +123,9 @@ void SingleEscapes() { } offset = 0; Require(u_unescapeAt_nid_postfix(nullptr, &offset, -1, nullptr) == -1 && offset == 0); - RequireThrows([&] { u_unescapeAt_nid_postfix(nullptr, &offset, 1, nullptr); }); - RequireThrows([] { u_unescapeAt_nid_postfix(Read_nid_no_patch, nullptr, 1, nullptr); }); + RequireInvalidArgument([&] { u_unescapeAt_nid_postfix(nullptr, &offset, 1, nullptr); }); + RequireInvalidArgument([] { u_unescapeAt_nid_postfix(Read_nid_no_patch, nullptr, 1, nullptr); }); + RequireInvalidArgument([] { u_unescapeAt_nid_postfix(nullptr, nullptr, -1, nullptr); }); } void String(const char* escaped, std::u16string_view expected) { @@ -162,12 +170,10 @@ void Strings() { Require(u_unescape_nid_postfix("a\\u12", destination, 3) == 0 && destination[0] == 0 && destination[1] == 0xa55a); destination[0] = 0xa55a; Require(u_unescape_nid_postfix("abc\\u12", destination, 3) == 0 && destination[0] == 0 && destination[1] == u'b' && destination[2] == u'c'); - for (auto capacity : {-1, std::numeric_limits::min()}) { - std::fill(std::begin(destination), std::end(destination), 0xa55a); - RequireThrows([&] { u_unescape_nid_postfix("a\\U0001f600", destination, capacity); }); - Require(destination[0] == 0xa55a && destination[1] == 0xa55a && destination[2] == 0xa55a); - } - RequireThrows([] { u_unescape_nid_postfix(nullptr, nullptr, 0); }); + std::fill(std::begin(destination), std::end(destination), 0xa55a); + RequireInvalidArgument([] { u_unescape_nid_postfix(nullptr, nullptr, 0); }); + RequireInvalidArgument([&] { u_unescape_nid_postfix(nullptr, destination, 3); }); + Require(destination[0] == 0xa55a && destination[1] == 0xa55a && destination[2] == 0xa55a); for (std::uint32_t value = 0; value <= 0x10ffff; value += 17) { char escaped[16]; std::snprintf(escaped, sizeof(escaped), "\\U%08x", value); @@ -179,11 +185,36 @@ void Strings() { } } +void NegativeCapacity() { + constexpr auto minimum = std::numeric_limits::min(); + struct Case { + const char* escaped; + std::int32_t capacity; + std::int32_t size; + }; + for (const auto& test : {Case{"a\\U0001f600", -1, 3}, Case{"a\\U0001f600", -2, 3}, Case{"a\\U0001f600", minimum + 1, 3}, + Case{"plain", -1, 5}, Case{"\\n", minimum, 1}, Case{"ab", minimum, 2}, Case{"a\\u12", minimum, 0}, + Case{"a\\u12", -1, 0}, Case{"ab\\n", minimum + 2, 3}, Case{"\\nab", minimum + 1, 3}, Case{"", minimum, 0}}) { + std::array output; + output.fill(0xa55a); + Require(u_unescape_nid_postfix(test.escaped, output.data() + 1, test.capacity) == test.size); + Require(std::all_of(output.begin(), output.end(), [](char16_t unit) { return unit == 0xa55a; })); + } + for (const auto& test : {Case{"a\\n", minimum, 0}, Case{"\\na", minimum, 0}, Case{"ab\\n", minimum + 1, 0}, Case{"\\nab", minimum, 0}}) { + std::array output; + output.fill(0xa55a); + RequireThrows([&] { u_unescape_nid_postfix(test.escaped, output.data() + 1, test.capacity); }); + Require(std::all_of(output.begin(), output.end(), [](char16_t unit) { return unit == 0xa55a; })); + Require(u_unescape_nid_postfix(test.escaped, nullptr, test.capacity) == static_cast(std::string_view(test.escaped).size()) - 1); + } +} + } // namespace int main() { SingleEscapes(); CallbackOrder(); Strings(); + NegativeCapacity(); std::puts("ICU escape decoding tests passed"); } diff --git a/docs/dev/debt/libSceIcu.md b/docs/dev/debt/libSceIcu.md index c9e6d55d7a..df58daf59f 100644 --- a/docs/dev/debt/libSceIcu.md +++ b/docs/dev/debt/libSceIcu.md @@ -8,7 +8,7 @@ - [C string helpers](../../../core/libs/prx/libSceIcu/CString.cpp) have no console ABI or behavior verification. Their names/NIDs are in the PS5 3.20 inventory; signatures and behavior follow ICU 78.1's [cstring.cpp](https://github.com/unicode-org/icu/blob/049e0d6a420629ac7db77256987d083a563287b5/icu4c/source/common/cstring.cpp), unchanged since 65.1 apart from casts. Formatting accepts radices 2–16; null buffers or other radices throw an invalid-argument exception, whereas ICU dereferences the buffer and only asserts the radix. Signed minima and outputs exceeding ICU's 30-byte temporary use unsigned arithmetic and caller storage without reproducing upstream undefined behavior. Parsing uses guest LP64 `strtoul` and keeps the low 32 bits; a null source throws. Cross-firmware availability, native invalid-input behavior and the undefined upstream cases remain unverified. -- [Escape decoding](../../../core/libs/prx/libSceIcu/Unescape.cpp) has no behavior or ABI verification on console hardware. It follows ICU 78.1's [ustring.cpp](https://github.com/unicode-org/icu/blob/049e0d6a420629ac7db77256987d083a563287b5/icu4c/source/common/ustring.cpp), including rejection of eight-digit braced hexadecimal escapes although its public header advertises up to eight digits. Null source or offset pointers, null callbacks with an in-range offset, negative capacity with a nonnull destination and source lengths exceeding `INT32_MAX` throw. Null-destination preflight ignores capacity. Closes when: compared with the console, including callback ABI, byte conversion, capacity limits and braced hexadecimal parsing. +- [Escape decoding](../../../core/libs/prx/libSceIcu/Unescape.cpp) has no behavior or ABI verification on console hardware. It follows ICU 78.1's [ustring.cpp](https://github.com/unicode-org/icu/blob/049e0d6a420629ac7db77256987d083a563287b5/icu4c/source/common/ustring.cpp), including rejection of eight-digit braced hexadecimal escapes although its public header advertises up to eight digits. Null source or offset pointers and null callbacks with an in-range offset, which ICU dereferences, throw `std::invalid_argument`. A negative capacity writes nothing, as in ICU; source lengths exceeding `INT32_MAX` and a capacity that overflows ICU's signed `destCapacity - i` throw. Null-destination preflight ignores capacity. Closes when: compared with the console, including callback ABI, byte conversion, capacity limits and braced hexadecimal parsing. - [Invariant character and version helpers](../../../core/libs/prx/libSceIcu/Version.cpp) are unverified on a console. `u_charsToUChars`, `u_UCharsToChars`, `u_versionFromString`, `u_versionFromUString` and `u_versionToString` follow ICU 78.1 [uinvchar.cpp](https://github.com/unicode-org/icu/blob/release-78.1/icu4c/source/common/uinvchar.cpp) and [putil.cpp](https://github.com/unicode-org/icu/blob/release-78.1/icu4c/source/common/putil.cpp), unchanged since 65.1. Firmware 13.42 reports ICU 78.1 and exports none of them ([#11013](https://github.com/boykopovar/AnyPS5/pull/11013#issuecomment-6400470698)); the [3.20 table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceIcu.c) has all five. Version parts use the guest `strtoul`, truncated to eight bits; `u_versionToString` keeps ICU's dropped zero tens digit (105 becomes `15`). A null buffer with a positive conversion length throws; ICU dereferences it. Closes when: compared with the console. From b469fc13155b14033b5aff578b373cb0bfef57c3 Mon Sep 17 00:00:00 2001 From: plexerPK <160128888+plexerPK@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:39:13 +0100 Subject: [PATCH 129/160] fix(tools): exclude CMake test metadata from script registration --- tools/check_test_registration.py | 9 +++++++++ tools/tests/test_test_registration.py | 24 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index 426a9546d0..4aff0d4858 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -181,6 +181,15 @@ def run_scripts(root, files): content = (Path(root) / name).read_text(encoding="utf-8", errors="replace") for command, arguments in commands(content): if command == "add_test": + if arguments and arguments[0] == "NAME": + if "COMMAND" not in arguments[2:]: + continue + start = arguments.index("COMMAND", 2) + 1 + end = next((index for index in range(start, len(arguments)) + if arguments[index] in ("CONFIGURATIONS", "WORKING_DIRECTORY", "COMMAND_EXPAND_LISTS", "BUILD_DEPENDS")), len(arguments)) + arguments = arguments[start:end] + else: + arguments = arguments[1:] run.update(argument.rpartition("}")[2].lstrip("/") for argument in arguments) elif name.startswith(".github/workflows/") and name.endswith((".yml", ".yaml")): for line in workflow_run_lines((Path(root) / name).read_text(encoding="utf-8", errors="replace")): diff --git a/tools/tests/test_test_registration.py b/tools/tests/test_test_registration.py index de2b729c21..fc46e5f40a 100644 --- a/tools/tests/test_test_registration.py +++ b/tools/tests/test_test_registration.py @@ -382,6 +382,30 @@ class ScriptRegistrationTests(unittest.TestCase): "core/relinker/CMakeLists.txt": "if(WIN32)\n add_test(NAME a COMMAND ${Python3_EXECUTABLE}\n ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_a.py)\nendif()\n", "core/relinker/relinker/tests/test_a.py": ""}), []) + def test_cmake_test_names_do_not_register_scripts(self): + for command in ('add_test(NAME test_a.py COMMAND python helper.py)', + 'add_test(test_a.py python helper.py)'): + with self.subTest(command=command): + self.assertEqual(self.unrun({"CMakeLists.txt": command, "test_a.py": ""}), ["test_a.py"]) + + def test_cmake_test_options_do_not_register_scripts(self): + for option in ("CONFIGURATIONS", "WORKING_DIRECTORY", "BUILD_DEPENDS"): + for command in (f'add_test(NAME a {option} test_a.py COMMAND python helper.py)', + f'add_test(NAME a COMMAND python helper.py {option} test_a.py)'): + with self.subTest(command=command): + self.assertEqual(self.unrun({"CMakeLists.txt": command, "test_a.py": ""}), ["test_a.py"]) + + def test_cmake_command_scripts_remain_registered(self): + commands = ('add_test(a python test_a.py)', + 'add_test(NAME a CONFIGURATIONS Debug COMMAND python test_a.py)', + 'add_test(NAME a WORKING_DIRECTORY work COMMAND python test_a.py)', + 'add_test(NAME a COMMAND python test_a.py CONFIGURATIONS Debug)', + 'add_test(NAME a COMMAND python test_a.py COMMAND_EXPAND_LISTS)', + 'add_test(NAME a COMMAND test_a.py WORKING_DIRECTORY work)') + for command in commands: + with self.subTest(command=command): + self.assertEqual(self.unrun({"CMakeLists.txt": command, "test_a.py": ""}), []) + def test_script_in_bracket_argument_with_initial_newline_is_registered(self): self.assertEqual(self.unrun({ "CMakeLists.txt": "add_test(NAME a COMMAND python [=[\ntools/tests/test_a.py]=])\n", From ad01e525c21e1ca587ab5556d6fc552adecc8ce2 Mon Sep 17 00:00:00 2001 From: mrph28 <12759261+mrph28@users.noreply.github.com> Date: Wed, 28 Oct 2026 13:55:30 -0300 Subject: [PATCH 130/160] feat(input): complete the Portuguese messages of the input mapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translate the 32 message keys the Portuguese catalogue lacked: the skip link, the navigation and section labels, the replace and capture dialogs, the import, clipboard and download statuses, and the per-binding accessibility labels. Use "atribuição" for a binding and keep "entrada" for a keyboard or mouse input in the strings that already said "entrada". --- tools/input-mapper/i18n.js | 40 ++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/tools/input-mapper/i18n.js b/tools/input-mapper/i18n.js index 3f6b3f0d86..4748a064e2 100644 --- a/tools/input-mapper/i18n.js +++ b/tools/input-mapper/i18n.js @@ -483,15 +483,15 @@ export const messages = { "group4": "Analógico direito", "group5": "Toque e utilidades", "select": "Selecionar…", - "add": "+ Adicionar entrada", + "add": "+ Adicionar atribuição", "capture": "Capturar", "remove": "Remover", "builtin": "Padrão: {inputs}", - "custom": "As entradas personalizadas substituem as entradas padrão.", + "custom": "As atribuições personalizadas substituem as atribuições padrão.", "noResults": "Nenhum controle encontrado.", "download": "Baixar INI", "copy": "Copiar configuração", - "previewEmpty": "Adicione uma entrada para começar.", + "previewEmpty": "Adicione uma atribuição para começar.", "backControls": "Voltar aos controles", "switchGroup": "Trocar grupo", "moveFocus": "Mover o foco", @@ -502,13 +502,45 @@ export const messages = { "keepEditing": "Continuar editando", "discard": "Descartar alterações", "faq0Question": "Como atribuo uma tecla?", - "faq0Answer": "Escolha um grupo e Adicionar entrada. Use Capturar ou a lista. Uma ação pode ter várias entradas alternativas.", + "faq0Answer": "Escolha um grupo e Adicionar atribuição. Use Capturar ou a lista. Uma ação pode ter várias atribuições alternativas.", "faq1Question": "Onde coloco o arquivo INI?", "faq1Answer": "Coloque anyps5-input.ini junto do executável gerado do jogo. Para outro caminho, use ANYPS5_INPUT_CONFIG.", "faq2Question": "Por que uma tecla não é capturada?", "faq2Answer": "O navegador ou sistema pode interceptá-la. Escolha na lista. Escape cancela a captura; atribua Escape pela lista.", "faq3Question": "As alterações são salvas sozinhas?", "faq3Answer": "As alterações ficam na memória: copie ou baixe antes de recarregar. Só o idioma é salvo. Ações não configuradas mantêm os padrões.", + "skip": "Pular para o conteúdo", + "mainNav": "Navegação principal", + "groups": "Grupos de controles", + "find": "Buscar uma ação", + "clock": "Hora local", + "output": "Prévia da configuração", + "captureHelp": "A captura usa as posições físicas das teclas. Escape cancela a captura; escolha Escape na lista para atribuí-lo. Alguns atalhos do navegador e do sistema exigem seleção manual.", + "replaceTitle": "Descartar alterações não salvas?", + "replaceHelp": "Copie ou baixe a configuração antes, se quiser mantê-la.", + "captureCancelled": "Captura cancelada.", + "captureBlur": "Captura cancelada porque a janela perdeu o foco.", + "captured": "{value} atribuído a {action}.", + "resetDone": "Configuração redefinida.", + "importRejected": "Importação rejeitada", + "importRead": "Importação lida com sucesso", + "line": "Linha {line}: {message}", + "preserved": "Sua configuração atual foi preservada.", + "imported": "{name} importado com sucesso.", + "importFailed": "Falha na importação: {message}", + "fileTooLarge": "O arquivo INI deve ter menos de 1 MiB.", + "invalidUtf8": "O arquivo deve ser texto UTF-8 válido.", + "downloaded": "Download da configuração solicitado.", + "copied": "Configuração copiada.", + "clipboardFailed": "Área de transferência indisponível. Copie a prévia selecionada com Ctrl+C.", + "noScript": "Ative o JavaScript para usar este configurador.", + "noDefault": "nenhuma entrada de teclado ou mouse", + "bindingLabel": "{action}, atribuição {number}", + "typeLabel": "{binding}: tipo", + "valueLabel": "{binding}: valor", + "addLabel": "Adicionar atribuição a {action}", + "captureLabel": "Capturar tecla para {binding}", + "removeLabel": "Remover {binding}", "searchOptions": "Buscar opções", "noOptions": "Nenhuma opção encontrada." }, From 0974e1378e1fe2d8046b025f0da367176917c1ab Mon Sep 17 00:00:00 2001 From: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:04:36 -0700 Subject: [PATCH 131/160] fix(io): stage writes through existing output symlinks --- core/relinker/CMakeLists.txt | 15 ++ core/relinker/io/src/FileWriter.cpp | 9 +- .../io/tests/SymlinkFileWriterTests.cpp | 145 ++++++++++++++++++ 3 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 core/relinker/io/tests/SymlinkFileWriterTests.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index fa2d17897a..60efcda745 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -423,6 +423,21 @@ if(BUILD_TESTING) set_tests_properties(windows_file_writer_binary windows_file_writer_text PROPERTIES TIMEOUT 20) endif() + if(UNIX) + add_test_executable(symlink_file_writer_tests + ${CMAKE_CURRENT_SOURCE_DIR}/io/tests/SymlinkFileWriterTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileWriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/NativePath.cpp + ) + target_include_directories(symlink_file_writer_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ) + add_test(NAME symlink_file_writer_binary COMMAND symlink_file_writer_tests binary) + add_test(NAME symlink_file_writer_text COMMAND symlink_file_writer_tests text) + set_tests_properties(symlink_file_writer_binary symlink_file_writer_text PROPERTIES TIMEOUT 20) + endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") add_test_executable(file_writer_tests ${CMAKE_CURRENT_SOURCE_DIR}/io/tests/FileWriterTests.cpp diff --git a/core/relinker/io/src/FileWriter.cpp b/core/relinker/io/src/FileWriter.cpp index 10606d09bb..c37c5836a4 100644 --- a/core/relinker/io/src/FileWriter.cpp +++ b/core/relinker/io/src/FileWriter.cpp @@ -57,9 +57,14 @@ bool writeAll(const int descriptor, const char* data, std::size_t size) { return true; } -bool replaceFile(const std::filesystem::path& target, const std::string& path, const char* data, const std::size_t size) { +bool replaceFile(std::filesystem::path target, const std::string& path, const char* data, const std::size_t size) { std::error_code error; - const auto previous = std::filesystem::symlink_status(target, error); + auto previous = std::filesystem::symlink_status(target, error); + if (std::filesystem::is_symlink(previous)) { + target = std::filesystem::canonical(target, error); + if (error) return false; + previous = std::filesystem::symlink_status(target, error); + } const bool regular = std::filesystem::is_regular_file(previous); if (!regular && previous.type() != std::filesystem::file_type::not_found) return false; diff --git a/core/relinker/io/tests/SymlinkFileWriterTests.cpp b/core/relinker/io/tests/SymlinkFileWriterTests.cpp new file mode 100644 index 0000000000..f603c521db --- /dev/null +++ b/core/relinker/io/tests/SymlinkFileWriterTests.cpp @@ -0,0 +1,145 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +std::string readFile(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +class TemporaryDirectory { +public: + std::filesystem::path Path; + + TemporaryDirectory() { + auto name = (std::filesystem::temp_directory_path() / "anyps5-symlink-writer-XXXXXX").string(); + require(mkdtemp(name.data()) != nullptr, "Cannot create test directory"); + Path = name; + } + + ~TemporaryDirectory() { + std::error_code error; + std::filesystem::remove_all(Path, error); + } +}; + +template +void checkReplacement(const std::filesystem::path& root, const TValue& data, const std::string& kind) { + const auto directory = root / kind; + std::filesystem::create_directories(directory / "targets"); + const auto target = directory / "targets" / "output"; + const auto link = directory / "link"; + const std::string previous = "previous output"; + std::ofstream(target, std::ios::binary) << previous; + const auto permissions = std::filesystem::perms::owner_all | std::filesystem::perms::group_read; + std::filesystem::permissions(target, permissions); + if (kind == "absolute") { + std::filesystem::create_symlink(std::filesystem::absolute(target), link); + } else if (kind == "relative") { + std::filesystem::create_symlink("targets/output", link); + } else { + std::filesystem::create_symlink("output", directory / "targets" / "indirect"); + std::filesystem::create_symlink("targets/indirect", link); + } + const auto linkValue = std::filesystem::read_symlink(link); + const auto child = fork(); + require(child >= 0, "Cannot fork write-failure probe"); + if (child == 0) { + rlimit limit{}; + if (getrlimit(RLIMIT_FSIZE, &limit) != 0) _exit(3); + limit.rlim_cur = 1; + if (std::signal(SIGXFSZ, SIG_IGN) == SIG_ERR || setrlimit(RLIMIT_FSIZE, &limit) != 0) _exit(4); + try { + Io::FileWriter().Write(link.string(), data); + } catch (const Domain::RelinkerException& error) { + _exit(std::string(error.what()) == "Failed to write file: " + link.string() ? 0 : 5); + } catch (...) { + _exit(6); + } + _exit(7); + } + int status = 0; + require(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0, + "Write failure did not report the requested symlink path"); + require(readFile(target) == previous, kind + " failed write changed the previous target"); + require(std::filesystem::read_symlink(link) == linkValue, "Failed write changed the symlink"); + const auto alias = directory / "old-output"; + std::filesystem::create_hard_link(target, alias); + std::ifstream opened(target, std::ios::binary); + require(opened.is_open(), "Cannot open the previous target"); + const auto mask = umask(077); + try { + Io::FileWriter().Write(link.string(), data); + } catch (...) { + umask(mask); + throw; + } + umask(mask); + require(readFile(target) == std::string(data.begin(), data.end()), "Target was not replaced"); + require(std::filesystem::read_symlink(link) == linkValue, "Successful write replaced the symlink"); + require((std::filesystem::status(target).permissions() & std::filesystem::perms::all) == permissions, + "Replacement changed the target permissions"); + require(readFile(alias) == previous, "Replacement rewrote another hard link"); + require(std::string(std::istreambuf_iterator(opened), std::istreambuf_iterator()) == previous, + "Replacement changed the previous open file"); + if (kind == "chained") + require(std::filesystem::read_symlink(directory / "targets" / "indirect") == "output", "Intermediate link changed"); + for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) + require(!entry.path().filename().string().starts_with(".anyps5-"), "Temporary file left behind"); +} + +template +void checkFallbacks(const std::filesystem::path& directory, const TValue& data) { + const auto dangling = directory / "dangling"; + std::filesystem::create_symlink("new-output", dangling); + Io::FileWriter().Write(dangling.string(), data); + require(std::filesystem::read_symlink(dangling) == "new-output", "Dangling link was replaced"); + require(readFile(directory / "new-output") == std::string(data.begin(), data.end()), "Dangling target not written"); + const auto device = directory / "device"; + std::filesystem::create_symlink("/dev/null", device); + Io::FileWriter().Write(device.string(), data); + require(std::filesystem::read_symlink(device) == "/dev/null", "Device symlink was replaced"); +} + +template +void check(const TValue& data) { + TemporaryDirectory directory; + for (const auto& kind : {"absolute", "relative", "chained"}) + checkReplacement(directory.Path, data, kind); + checkFallbacks(directory.Path, data); +} + +} + +int main(int argc, char* argv[]) { + try { + require(argc == 2, "Expected binary or text"); + for (const std::size_t size : {std::size_t{3}, std::size_t{65536}}) { + if (std::string(argv[1]) == "binary") check(std::vector(size, 0x41)); + else if (std::string(argv[1]) == "text") check(std::string(size, 'A')); + else throw std::runtime_error("Expected binary or text"); + } + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } + return 0; +} From 1ccdec96e718dfbc2d35317363992269668d1ac7 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 28 Oct 2026 13:06:31 -0400 Subject: [PATCH 132/160] fix(build): stop relinking guest_backtrace_tests on every Windows build guest_backtrace_tests set ENABLE_EXPORTS on every platform. On MinGW that makes CMake declare an import library, libguest_backtrace_tests.dll.a, as a link output, but GNU ld writes no import library for an executable that exports nothing. The output never exists, so every build, including a no-op one, relinked the test. Only the Linux and macOS path needs the exports, for dladdr to resolve the test's own symbol. The Windows path finds its module with GetModuleHandleExA, so the property is now set off Windows only. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index b20cc24b4d..b960122e6c 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -1164,8 +1164,10 @@ if(BUILD_TESTING) target_include_directories(guest_backtrace_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_backtrace_tests PRIVATE libc) configure_windows_unwind(guest_backtrace_tests) - set_target_properties(guest_backtrace_tests PROPERTIES ENABLE_EXPORTS ON) add_test(NAME guest_backtrace COMMAND guest_backtrace_tests) + if(NOT WIN32) + set_target_properties(guest_backtrace_tests PROPERTIES ENABLE_EXPORTS ON) + endif() if(WIN32) set_tests_properties(guest_backtrace PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") From c0ad523e33b4d91c6d7c0e65df47c3849394b076 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 28 Oct 2026 19:06:39 +0100 Subject: [PATCH 133/160] docs(libSceRazorCpu): list the calls that return without recording anything Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceRazorCpu.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/dev/debt/libSceRazorCpu.md b/docs/dev/debt/libSceRazorCpu.md index 092aa68738..eee8a19074 100644 --- a/docs/dev/debt/libSceRazorCpu.md +++ b/docs/dev/debt/libSceRazorCpu.md @@ -3,6 +3,15 @@ ### Silent stubs - [libSceRazorCpu](../../../core/libs/prx/libSceRazorCpu/Export.cpp): capture-controller registration, thread-local capture storage, marker serialization and flushing are not implemented. The host implements only the confirmed uninitialized-profiler path: markers return zero without reading labels, and `sceRazorCpuFlushOccurred` returns false without writing its optional output. +- [libSceRazorCpu](../../../core/libs/prx/libSceRazorCpu/Export.cpp): the entry above covers only the markers and `sceRazorCpuFlushOccurred`. Fourteen further calls also return without reading their arguments or recording anything (`sceRazorCpuDisableFiberUserMarkers` returns nothing, the others zero). Three of them are imported by the 53 titles run here: + +| Call | Titles importing it (of 53) | +| --- | --- | +| `sceRazorCpuSync` | 1 | +| `sceRazorCpuPlotValue` | 2 | +| `sceRazorCpuWriteBookmark` | 2 | + +The other eleven are imported by none of them: `sceRazorCpuIsCapturing`, `sceRazorCpuNamedSync`, `sceRazorCpuDisableFiberUserMarkers`, `sceRazorCpuInitDataTags`, `sceRazorCpuShutdownDataTags`, `sceRazorCpuTagArray`, `sceRazorCpuTagBuffer`, `sceRazorCpuResizeTaggedBuffer`, `sceRazorCpuUnTagBuffer`, `sceRazorCpuBeginLogicalFileAccess` and `sceRazorCpuEndLogicalFileAccess`. ### Unknown function info From e8532d9ecca64d021f7a4d7372bf787a5382071f Mon Sep 17 00:00:00 2001 From: AnyPS5 Contributor Date: Wed, 28 Oct 2026 21:29:16 +0100 Subject: [PATCH 134/160] docs(libSceUserService): record unmeasured accessibility getters in technical debt --- docs/dev/debt/libSceUserService.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceUserService.md b/docs/dev/debt/libSceUserService.md index d694250136..6cf28ebb2b 100644 --- a/docs/dev/debt/libSceUserService.md +++ b/docs/dev/debt/libSceUserService.md @@ -9,5 +9,6 @@ - [sceUserServiceGetEvent](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - login and logout events queued dynamically on controller hotplug have no source and are assumed, not measured - [sceUserServiceGetAgeLevel](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - on a PS5 (firmware 13.60) the value is the age set in the user's parental restriction (0, 7, 12 and 15 were measured) and `0xFFFFFFFF` without one; a user who is not logged in returns `0x80960009` with or without an output. Parental restrictions are not modelled, so the initial user is always reported as unrestricted. PPSA12544 (Unity) only stores the value for its user profile. The one call in PPSA12544 (0x16a80a0), made only when bit 0x4 of its flags is set, writes the value to a field of its own object (offset 0x5170) and ends its chain of getters on any nonzero result; what it does with the value afterwards was not read. - [sceUserServiceGetAccessibilityChatTranscription, PressAndHoldDelay, TriggerEffect, Vibration, ZoomEnabled, ZoomFollowFocus](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - report 0, 0, 1, 1, 0 and 1, what a PS5 (firmware 13.60) reports for a user whose accessibility settings are all at their defaults; the console reports the user's own settings (a changed vibration intensity read 3), which are not modelled. What each number means is unknown. A user who is not logged in returns `0x80960009` with or without an output. One title (PPSA04354) disassembled for this entry calls all six in a row (first at 0xea0efa) with its user ID, only when a flag bit (0x10) is set; a result other than 0 from any of them ends the block and is returned, and only when all six return 0 does it store the six values in its object. +- [sceUserServiceGetAccessibilityKeyremapEnable, sceUserServiceGetAccessibilityZoom](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - values reported by a console and their formats are unmeasured, so both throw. - [sceUserServiceGetKeyboardType, sceUserServiceGetJapaneseInputType, sceUserServiceGetTraditionalChineseInputType](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - AnyPS5 stores no per-user keyboard setting, so the initial user reports the keyboard type of the configured system language (`ANYPS5_LANGUAGE`, English US by default) and 0 for the two input variants, and any other user id returns `USER_SERVICE_ERROR_UNKNOWN_USER`. The language-to-keyboard-type table is the repository owner's (languages 0 to 29); a valid language outside it, such as Ukrainian (30), reports 20 (US English). A value that is not a console language number is rejected by `sceSystemServiceParamGetInt` with an error naming `ANYPS5_LANGUAGE`; the call throws it and leaves the output untouched. The input variant is 0 because the owner's table gives 0 for Japanese and Traditional Chinese, the only languages needing one. `sceConvertKeycodeGetImeKeyboardType` consumes these values. - [sceUserServiceGetPlatformPrivacyWs1](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the platform privacy setting for an offline user without a linked PSN account is assumed to be 0 (not permitted) with success; actual console privacy querying is not modelled. Return of `0x80960009` (`USER_SERVICE_ERROR_NOT_LOGGED_IN`) with or without an output for a user who is not logged in is borrowed from `sceUserServiceGetAgeLevel` and is not measured for this call. From 78f3d63c2de2abbeb41b3f1505526d19f12117c5 Mon Sep 17 00:00:00 2001 From: FelixKunzJr Date: Wed, 28 Oct 2026 00:24:02 +0100 Subject: [PATCH 135/160] docs(relinker): record Rosetta instruction support on an M2 Pro with macOS 27.0.1 --- docs/dev/debt/relinker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 48d5f103f5..726eebb47d 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -44,7 +44,7 @@ - macOS output still writes pointer fixups into executable segments. `dyld_info -fixups` reports each as `pointer rebase is in executable segment`, next to `pointer rebase is in non-writable segment` for a fixup in a segment without write access, which the [macOS image writer](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp) rejects. A segment is executable and writable when its `PT_LOAD` is `PF_R|PF_W|PF_X`, or when a writable `PT_LOAD` shares a 4 KiB page with an executable one, because each page takes the union of the flags of the `PT_LOAD`s that cover it. dyld on an M1 Pro running macOS 27.0.1 (x86-64 output under Rosetta 2, `LC_BUILD_VERSION` minos and sdk 11.0) loads such an executable, applies the fixup and runs it; a fixup into a `PF_R|PF_X` segment ended the process there with SIGBUS in `dyld4::Loader::applyFixupsGeneric` before `main`, not with a refusal. Not measured for a dylib or under another macOS or SDK version. - The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. - `--to-rosetta` lowers what Rosetta lacks on top of `--to-intel` ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). AVX2, FMA, BMI1/2, F16C and MOVBE were observed to execute on an M5 Max without being reported through CPUID; this is not a guarantee for other Rosetta hosts. ANDN and RDRAND fault on an M1 running macOS 15.3.1 and execute on an M1 running macOS 26.6.2, so on the M1 the difference follows the macOS version; on that host BEXTR, BLSI, BLSMSK, BLSR, TZCNT, LZCNT, POPCNT, BZHI, MULX, PDEP, PEXT, RORX, SARX, SHLX, SHRX, MOVBE, FMA, AVX2 and F16C execute as well, its CPUID leaf 1 reports none of MOVBE, FMA, AVX or F16C, and RDSEED, RDPID, CLWB, ADCX and ADOX raise SIGILL. They also execute on an M5 running macOS 26.4, where the rest of BMI1 and BMI2, LZCNT, PREFETCHW, CLFLUSHOPT, AVX, AVX2, FMA, F16C and MOVBE execute as well although CPUID leaf 7 reads zero and leaves 1 and 0x80000001 report none of them, and where RDSEED, CLWB, RDPID, ADCX, ADOX, SHA and AVX-512F raise an illegal instruction. On that host the BMI1 and BMI2 results match the Intel SDM definitions over 200,000 random inputs each for PDEP, PEXT, BEXTR, BZHI, LZCNT and TZCNT, so they compute the documented result and do not only avoid a fault. ANDN is lowered, but RDRAND and other BMI instructions still depend on the host. RDSEED becomes RDRAND and CLWB a NOP. RDPID returns processor 0, and RDTSCP runs unchanged with ECX set to 0 after it, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB (also measured on an M5, macOS 27.0.1). An RDPID or RDTSCP site with no movable instruction after it for the jump is kept; a kept RDTSCP reports 0xAAAAAAAB. ADCX/ADOX fault under Rosetta and are not converted: compilers do not emit them, and code that checks CPUID sees no ADX. The conversions are covered by the converter tests. ANDN lowering is also executed by an x86-64 test build under Rosetta on the M1; the other conversions were originally checked on an M5 Max, outside this repository's CI. -- `--to-rosetta` output faults on an M2 running macOS 14.8.9 when it reaches AVX, AVX2, FMA, F16C, RDRAND or a BMI1/BMI2 instruction other than ANDN, TZCNT and LZCNT: Rosetta on that host raises SIGILL for them and for RDSEED, RDPID, CLWB, ADCX and ADOX ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). Only TZCNT, LZCNT, POPCNT, MOVBE, CLFLUSHOPT, PREFETCHW and RDTSCP execute, and RDTSCP sets ECX to 0xAAAAAAAB. CPUID leaf 1 reports POPCNT and none of MOVBE, AVX, FMA, F16C or RDRAND, leaf 7 reads zero, and leaf 0x80000001 reports RDTSCP but neither LZCNT nor PREFETCHW. An M2 Max on macOS 27.2 reports the same CPUID but executes everything except RDSEED, RDPID, CLWB, ADCX and ADOX, so the faults follow the macOS version, not the chip. +- `--to-rosetta` output faults on an M2 running macOS 14.8.9 when it reaches AVX, AVX2, FMA, F16C, RDRAND or a BMI1/BMI2 instruction other than ANDN, TZCNT and LZCNT: Rosetta on that host raises SIGILL for them and for RDSEED, RDPID, CLWB, ADCX and ADOX ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). Only TZCNT, LZCNT, POPCNT, MOVBE, CLFLUSHOPT, PREFETCHW and RDTSCP execute, and RDTSCP sets ECX to 0xAAAAAAAB. CPUID leaf 1 reports POPCNT and none of MOVBE, AVX, FMA, F16C or RDRAND, leaf 7 reads zero, and leaf 0x80000001 reports RDTSCP but neither LZCNT nor PREFETCHW. An M2 Max on macOS 27.2 and an M2 Pro on macOS 27.0.1 report the same CPUID but execute everything except RDSEED, RDPID, CLWB, ADCX and ADOX, so the faults follow the macOS version, not the chip. - `--to-intel` replaces the approximate legacy SSE and VEX.128 register forms of RSQRTPS/RCPPS and VRSQRTPS/VRCPPS with [correctly rounded](../../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). Legacy SSE forms preserve the destination's upper YMM bits; VEX forms clear them. The 256-bit, scalar and memory forms keep the native instruction, as does a site shorter than 5 bytes with no movable instruction after it for the jump (5 sites in PPSA21564, counted before a following RIP-relative instruction could move into the stub; not recounted). - XSAVEC raises an illegal instruction under Rosetta and is not converted, because it writes the compacted format. XSAVEOPT, which also faults there, is rewritten to XSAVE in place by the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) under `--to-rosetta` (`0F AE /6` becomes `/4`; prefixes and operand are kept). The two differ only in the init and modified optimizations, which let XSAVEOPT skip writing unchanged or initial state ([Intel SDM volume 1](https://cdrdv2-public.intel.com/812382/253665-sdm-vol-1.pdf), chapter 13), so XRSTOR restores the same state from either area. Measured on one M5 with macOS 27.0.1: XSAVE and XRSTOR handle x87, SSE and AVX state in the standard format, although CPUID leaf 1 reports neither XSAVE nor OSXSAVE and leaf 0xD sub-leaf 1 reports neither XSAVEOPT nor XSAVEC. XSAVE also executes on an M5 with macOS 26.4, where it writes an XSTATE_BV of 0x7 and XGETBV returns the same x87, SSE and AVX mask although leaf 1 still reports neither XSAVE nor OSXSAVE; XRSTOR was not exercised there. The converter tests' x86-64 build executes the replacement XSAVE under Rosetta on that host. - `--trace-imports` ([Windows entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp), [Linux entry stub](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp), [macOS entry](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp)) leaves every import that is not a PLT slot or a function GOT slot untraced, and the same for a function reached through any other relocation, `sceKernelDlsym`, a weak import, or a module loaded at run time. On Linux the entry stub makes the page of every traced slot readable, writable and executable with `mprotect` before it stores the thunk, so a slot inside `PT_GNU_RELRO` can be replaced; those pages stay writable, and data pages become executable, while tracing is on. On macOS the stub writes the thunk into the bound slot and does not change its protection, because that slot stays in a writable segment From 0d608b579c6a673ffa9e26d25b3104aba598eccd Mon Sep 17 00:00:00 2001 From: slopgobblercobbler Date: Sun, 25 Oct 2026 18:26:47 +0100 Subject: [PATCH 136/160] fix(libSceSaveDataDialog.native): validate param size in sceSaveDataDialogOpen and condense debt entry --- docs/dev/debt/libSceSaveDataDialog.native.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceSaveDataDialog.native.md b/docs/dev/debt/libSceSaveDataDialog.native.md index 8ff8c8a7c8..3768ee90ab 100644 --- a/docs/dev/debt/libSceSaveDataDialog.native.md +++ b/docs/dev/debt/libSceSaveDataDialog.native.md @@ -2,5 +2,5 @@ ### Silent stubs -- [libSceSaveDataDialog.native](../../../core/libs/prx/libSceSaveDataDialog.native/Export.cpp) shows no UI and ignores progress values. Progress-bar mode (5) stays running until the caller closes it, matching Skyrim PPSA03747's save sequence; other modes still finish immediately and return button 1 and the first nonempty directory name. Console dialog timing and selection behavior remain unverified. Lifecycle state error codes follow shadPS4: `sceSaveDataDialogClose` returns `SAVE_DATA_DIALOG_ERROR_NOT_RUNNING` whenever not running, `sceSaveDataDialogGetResult` returns `SAVE_DATA_DIALOG_ERROR_NOT_FINISHED` whenever not finished (checked before null pointer), and `sceSaveDataDialogTerminate` closes running dialogs and returns OK, rejecting uninitialized calls with `SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED`, and progress updates (`sceSaveDataDialogProgressBarInc`, `sceSaveDataDialogProgressBarSetValue`) return `SAVE_DATA_DIALOG_ERROR_NOT_RUNNING` whenever not running. -- [sceSaveDataDialogIsReadyToDisplay](../../../core/libs/prx/libSceSaveDataDialog.native/Export.cpp) returns 1 without checking dialog initialization or display readiness. +- [libSceSaveDataDialog.native](../../../core/libs/prx/libSceSaveDataDialog.native/Export.cpp) shows no UI and ignores progress values. Mode 5 (progress) runs until closed (Skyrim PPSA03747); other modes finish immediately returning button 1 and first nonempty directory. Console timing and selection are unverified. Lifecycle errors follow shadPS4: `sceSaveDataDialogClose` and progress updates return `SAVE_DATA_DIALOG_ERROR_NOT_RUNNING` when not running, `sceSaveDataDialogGetResult` returns `SAVE_DATA_DIALOG_ERROR_NOT_FINISHED` before null checks, and `sceSaveDataDialogTerminate` closes running dialogs, returning `SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED` when uninitialized. Parameter size in `sceSaveDataDialogOpen` is unvalidated as console behavior is unmeasured. +- [sceSaveDataDialogIsReadyToDisplay](../../../core/libs/prx/libSceSaveDataDialog.native/Export.cpp) returns 1 without checking initialization or readiness. From e0e260357fef63833ef6d8905fe71fb1356a9230 Mon Sep 17 00:00:00 2001 From: slopgobblercobbler Date: Thu, 29 Oct 2026 00:41:16 +0100 Subject: [PATCH 137/160] docs(debt): document unmeasured sceSaveDataDialogOpen param size validation --- docs/dev/debt/libSceSaveDataDialog.native.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceSaveDataDialog.native.md b/docs/dev/debt/libSceSaveDataDialog.native.md index 3768ee90ab..b4c8901752 100644 --- a/docs/dev/debt/libSceSaveDataDialog.native.md +++ b/docs/dev/debt/libSceSaveDataDialog.native.md @@ -2,5 +2,6 @@ ### Silent stubs -- [libSceSaveDataDialog.native](../../../core/libs/prx/libSceSaveDataDialog.native/Export.cpp) shows no UI and ignores progress values. Mode 5 (progress) runs until closed (Skyrim PPSA03747); other modes finish immediately returning button 1 and first nonempty directory. Console timing and selection are unverified. Lifecycle errors follow shadPS4: `sceSaveDataDialogClose` and progress updates return `SAVE_DATA_DIALOG_ERROR_NOT_RUNNING` when not running, `sceSaveDataDialogGetResult` returns `SAVE_DATA_DIALOG_ERROR_NOT_FINISHED` before null checks, and `sceSaveDataDialogTerminate` closes running dialogs, returning `SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED` when uninitialized. Parameter size in `sceSaveDataDialogOpen` is unvalidated as console behavior is unmeasured. -- [sceSaveDataDialogIsReadyToDisplay](../../../core/libs/prx/libSceSaveDataDialog.native/Export.cpp) returns 1 without checking initialization or readiness. +- [libSceSaveDataDialog.native](../../../core/libs/prx/libSceSaveDataDialog.native/Export.cpp) shows no UI and ignores progress values. Progress-bar mode (5) stays running until the caller closes it, matching Skyrim PPSA03747's save sequence; other modes still finish immediately and return button 1 and the first nonempty directory name. Console dialog timing and selection behavior remain unverified. Lifecycle state error codes follow shadPS4: `sceSaveDataDialogClose` returns `SAVE_DATA_DIALOG_ERROR_NOT_RUNNING` whenever not running, `sceSaveDataDialogGetResult` returns `SAVE_DATA_DIALOG_ERROR_NOT_FINISHED` whenever not finished (checked before null pointer), and `sceSaveDataDialogTerminate` closes running dialogs and returns OK, rejecting uninitialized calls with `SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED`, and progress updates (`sceSaveDataDialogProgressBarInc`, `sceSaveDataDialogProgressBarSetValue`) return `SAVE_DATA_DIALOG_ERROR_NOT_RUNNING` whenever not running. + Parameter size in `sceSaveDataDialogOpen` is unvalidated as console behaviour is unmeasured. +- [sceSaveDataDialogIsReadyToDisplay](../../../core/libs/prx/libSceSaveDataDialog.native/Export.cpp) returns 1 without checking dialog initialization or display readiness. From d2ce672d13abbaa75aa44718610fb582eb517f86 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:06:59 -0300 Subject: [PATCH 138/160] feat(libSceFreeType): support per-face x-height limits --- .../libSceFreeType/include/GuestFreeType.hpp | 6 ++ .../libSceFreeType/src/ModuleProperties.cpp | 30 +++++++- .../tests/GuestFreeTypeModuleProperties.cpp | 76 +++++++++++++++++++ docs/dev/debt/libSceFreeType.md | 2 +- 4 files changed, 112 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp b/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp index 51acae2148..67af76996e 100644 --- a/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp +++ b/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp @@ -184,6 +184,11 @@ struct GuestFaceRec { void* internal; }; +struct GuestPropIncreaseXHeight { + GuestFaceRec* face; + std::uint32_t limit; +}; + static_assert(sizeof(GuestMatrix) == 32 && offsetof(GuestMatrix, xy) == 8 && offsetof(GuestMatrix, yx) == 16 && offsetof(GuestMatrix, yy) == 24); static_assert(sizeof(GuestGeneric) == 16 && offsetof(GuestGeneric, finalizer) == 8); static_assert(sizeof(GuestBitmapSize) == 32 && offsetof(GuestBitmapSize, size) == 8); @@ -205,6 +210,7 @@ static_assert(offsetof(GuestFaceRec, num_fixed_sizes) == 56 && offsetof(GuestFac static_assert(offsetof(GuestFaceRec, bbox) == 104 && offsetof(GuestFaceRec, units_per_EM) == 136 && offsetof(GuestFaceRec, underline_thickness) == 150); static_assert(offsetof(GuestFaceRec, glyph) == 0x98 && offsetof(GuestFaceRec, size) == 0xa0 && offsetof(GuestFaceRec, charmap) == 0xa8); static_assert(offsetof(GuestFaceRec, autohint) == 216 && offsetof(GuestFaceRec, internal) == 240 && sizeof(GuestFaceRec) == 248); +static_assert(sizeof(GuestPropIncreaseXHeight) == 16 && offsetof(GuestPropIncreaseXHeight, face) == 0 && offsetof(GuestPropIncreaseXHeight, limit) == 8); } diff --git a/core/libs/prx/libSceFreeType/src/ModuleProperties.cpp b/core/libs/prx/libSceFreeType/src/ModuleProperties.cpp index a8ac8bd3c8..1932227852 100644 --- a/core/libs/prx/libSceFreeType/src/ModuleProperties.cpp +++ b/core/libs/prx/libSceFreeType/src/ModuleProperties.cpp @@ -3,6 +3,7 @@ #include "prx/libc/include/General.hpp" #include FT_MODULE_H +#include FT_DRIVER_H #include #include @@ -16,7 +17,7 @@ static_assert(sizeof(FT_Bool) == sizeof(std::uint8_t)); namespace { -enum class PropertyType { Unsigned, Signed, Boolean, Darkening, Unsupported }; +enum class PropertyType { Unsigned, Signed, Boolean, Darkening, IncreaseXHeight, Unsupported }; using DarkeningParameters = FT_Int[8]; PropertyType TypeOf(const char* moduleName, const char* propertyName) { @@ -36,6 +37,7 @@ PropertyType TypeOf(const char* moduleName, const char* propertyName) { std::strcmp(propertyName, "default-script") == 0)) { return PropertyType::Unsigned; } + if (autoHinter && std::strcmp(propertyName, "increase-x-height") == 0) return PropertyType::IncreaseXHeight; return PropertyType::Unsupported; } @@ -47,6 +49,20 @@ int Read(FT_Library library, const char* moduleName, const char* propertyName, v return error; } +int ReadIncreaseXHeight(FT_Library library, const char* moduleName, const char* propertyName, void* output) { + GuestFaceRec* guestFace = nullptr; + std::memcpy(&guestFace, output, sizeof(guestFace)); + const auto face = FaceOf(guestFace); + FT_Prop_IncreaseXHeight hostProp{}; + hostProp.face = face ? face->host : nullptr; + const auto error = FT_Property_Get(library, moduleName, propertyName, &hostProp); + if (!error) { + const std::uint32_t limit = hostProp.limit; + std::memcpy(static_cast(output) + offsetof(GuestPropIncreaseXHeight, limit), &limit, sizeof(limit)); + } + return error; +} + template int Write(FT_Library library, const char* moduleName, const char* propertyName, const void* input) { TValue value{}; @@ -54,6 +70,16 @@ int Write(FT_Library library, const char* moduleName, const char* propertyName, return FT_Property_Set(library, moduleName, propertyName, &value); } +int WriteIncreaseXHeight(FT_Library library, const char* moduleName, const char* propertyName, const void* input) { + GuestPropIncreaseXHeight guestProp{}; + std::memcpy(&guestProp, input, sizeof(guestProp)); + const auto face = FaceOf(guestProp.face); + FT_Prop_IncreaseXHeight hostProp{}; + hostProp.face = face ? face->host : nullptr; + hostProp.limit = guestProp.limit; + return FT_Property_Set(library, moduleName, propertyName, &hostProp); +} + } extern "C" int APS5_VABI FT_Property_Get_nid_postfix(GuestLibrary* library, const char* moduleName, @@ -65,6 +91,7 @@ extern "C" int APS5_VABI FT_Property_Get_nid_postfix(GuestLibrary* library, cons case PropertyType::Signed: return Read(host, moduleName, propertyName, value); case PropertyType::Boolean: return Read(host, moduleName, propertyName, value); case PropertyType::Darkening: return Read(host, moduleName, propertyName, value); + case PropertyType::IncreaseXHeight: return ReadIncreaseXHeight(host, moduleName, propertyName, value); default: NotImplemented_nid_no_patch(__func__); } } @@ -78,6 +105,7 @@ extern "C" int APS5_VABI FT_Property_Set_nid_postfix(GuestLibrary* library, cons case PropertyType::Signed: return Write(host, moduleName, propertyName, value); case PropertyType::Boolean: return Write(host, moduleName, propertyName, value); case PropertyType::Darkening: return Write(host, moduleName, propertyName, value); + case PropertyType::IncreaseXHeight: return WriteIncreaseXHeight(host, moduleName, propertyName, value); default: NotImplemented_nid_no_patch(__func__); } } diff --git a/core/libs/tests/GuestFreeTypeModuleProperties.cpp b/core/libs/tests/GuestFreeTypeModuleProperties.cpp index dfd6952e47..74534f2974 100644 --- a/core/libs/tests/GuestFreeTypeModuleProperties.cpp +++ b/core/libs/tests/GuestFreeTypeModuleProperties.cpp @@ -1,3 +1,4 @@ +#include "FreeTypeFontFixture.hpp" #include "prx/libSceFreeType/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" @@ -31,6 +32,8 @@ struct Api { int (APS5_VABI *done)(GuestLibrary*); int (APS5_VABI *get)(GuestLibrary*, const char*, const char*, void*); int (APS5_VABI *set)(GuestLibrary*, const char*, const char*, const void*); + int (APS5_VABI *newMemoryFace)(GuestLibrary*, const unsigned char*, std::int64_t, std::int64_t, FreeType::GuestFaceRec**); + int (APS5_VABI *doneFace)(FreeType::GuestFaceRec*); }; static void Require(bool condition, std::source_location location = std::source_location::current()) { @@ -78,6 +81,43 @@ static void Change(const Api& api, GuestLibrary* library, FT_Library native, Gue Query(api, other, nativeOther, module, property); } +static void QueryIncreaseXHeight(const Api& api, GuestLibrary* library, FT_Library native, + FreeType::GuestFaceRec* guestFace, FT_Face nativeFace) { + FT_Prop_IncreaseXHeight expected{}; + expected.face = nativeFace; + expected.limit = 0xA5A5A5A5u; + const int expectedError = FT_Property_Get(native, "autofitter", "increase-x-height", &expected); + std::array storage{}; + storage.fill(0xA5); + auto* carrier = storage.data() + 8; + std::memcpy(carrier, &guestFace, sizeof(guestFace)); + auto expectedStorage = storage; + if (!expectedError) { + std::memcpy(expectedStorage.data() + 8 + offsetof(FreeType::GuestPropIncreaseXHeight, limit), + &expected.limit, sizeof(expected.limit)); + } + const int actualError = api.get(library, "autofitter", "increase-x-height", carrier); + Require(actualError == expectedError); + Require(storage == expectedStorage); +} + +static void ChangeIncreaseXHeight(const Api& api, GuestLibrary* library, FT_Library native, + FreeType::GuestFaceRec* guestFace, FT_Face nativeFace, std::uint32_t limit) { + std::array input{}; + input.fill(0xCC); + auto* carrier = input.data() + 8; + std::memcpy(carrier, &guestFace, sizeof(guestFace)); + std::memcpy(carrier + offsetof(FreeType::GuestPropIncreaseXHeight, limit), &limit, sizeof(limit)); + const auto before = input; + FT_Prop_IncreaseXHeight nativeProp{}; + nativeProp.face = nativeFace; + nativeProp.limit = limit; + const int expectedError = FT_Property_Set(native, "autofitter", "increase-x-height", &nativeProp); + const int actualError = api.set(library, "autofitter", "increase-x-height", carrier); + Require(actualError == expectedError); + Require(input == before); +} + static void Run(const Api& api) { GuestLibrary* library = nullptr; GuestLibrary* other = nullptr; @@ -142,6 +182,40 @@ static void Run(const Api& api) { Require(output == before); Query(api, library, native, "truetype", "interpreter-version"); Query(api, other, nativeOther, "truetype", "interpreter-version"); + const auto fontBytes = SquareGlyphFont(65, 600); + FreeType::GuestFaceRec* face1 = nullptr; + FT_Face nativeFace1 = nullptr; + Require(api.newMemoryFace(library, fontBytes.data(), static_cast(fontBytes.size()), 0, &face1) == 0); + Require(FT_New_Memory_Face(native, fontBytes.data(), static_cast(fontBytes.size()), 0, &nativeFace1) == 0); + FreeType::GuestFaceRec* face2 = nullptr; + FT_Face nativeFace2 = nullptr; + Require(api.newMemoryFace(library, fontBytes.data(), static_cast(fontBytes.size()), 0, &face2) == 0); + Require(FT_New_Memory_Face(native, fontBytes.data(), static_cast(fontBytes.size()), 0, &nativeFace2) == 0); + FreeType::GuestFaceRec* face3 = nullptr; + FT_Face nativeFace3 = nullptr; + Require(api.newMemoryFace(other, fontBytes.data(), static_cast(fontBytes.size()), 0, &face3) == 0); + Require(FT_New_Memory_Face(nativeOther, fontBytes.data(), static_cast(fontBytes.size()), 0, &nativeFace3) == 0); + QueryIncreaseXHeight(api, library, native, face1, nativeFace1); + QueryIncreaseXHeight(api, library, native, face2, nativeFace2); + QueryIncreaseXHeight(api, other, nativeOther, face3, nativeFace3); + for (const auto limit : {0u, 6u, 20u, 42u, std::numeric_limits::max()}) { + ChangeIncreaseXHeight(api, library, native, face1, nativeFace1, limit); + QueryIncreaseXHeight(api, library, native, face1, nativeFace1); + QueryIncreaseXHeight(api, library, native, face2, nativeFace2); + } + ChangeIncreaseXHeight(api, library, native, face2, nativeFace2, 20u); + QueryIncreaseXHeight(api, library, native, face2, nativeFace2); + QueryIncreaseXHeight(api, library, native, face1, nativeFace1); + ChangeIncreaseXHeight(api, library, native, face3, nativeFace3, 42u); + QueryIncreaseXHeight(api, library, native, face3, nativeFace3); + QueryIncreaseXHeight(api, other, nativeOther, face3, nativeFace3); + QueryIncreaseXHeight(api, library, native, face1, nativeFace1); + QueryIncreaseXHeight(api, library, native, face2, nativeFace2); + ChangeIncreaseXHeight(api, library, native, nullptr, nullptr, 99u); + QueryIncreaseXHeight(api, library, native, nullptr, nullptr); + Require(api.doneFace(face1) == 0 && FT_Done_Face(nativeFace1) == 0); + Require(api.doneFace(face2) == 0 && FT_Done_Face(nativeFace2) == 0); + Require(api.doneFace(face3) == 0 && FT_Done_Face(nativeFace3) == 0); Require(api.done(library) == 0 && api.done(other) == 0); Require(FT_Done_FreeType(native) == 0 && FT_Done_FreeType(nativeOther) == 0); } @@ -175,6 +249,8 @@ int main(int argc, char** argv) { api.done = Import(module, "FT_Done_FreeType", "dH02CpEEcYk", patched); api.get = Import(module, "FT_Property_Get", "8fAMdZCnewA", patched); api.set = Import(module, "FT_Property_Set", "LsB538+IPrE", patched); + api.newMemoryFace = Import(module, "FT_New_Memory_Face", "40Wa2neSLM0", patched); + api.doneFace = Import(module, "FT_Done_Face", "OgFczHko0w8", patched); Run(api); #ifdef _WIN32 Require(FreeLibrary(module) != 0); diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index af4fa3a000..dd26809634 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -41,7 +41,7 @@ - [FT_Get_Charmap_Index, FT_Get_CMap_Language_ID, FT_Get_CMap_Format](../../../core/libs/prx/libSceFreeType/src/Charmap.cpp) (libSceFreeType) - console signatures and return values are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c) and the existing guest LP64 model. Charmaps must be the face's original read-only guest records; copied or otherwise unregistered records with a non-null face throw instead of reading private native cmap fields from guest memory -- [FT_Property_Get, FT_Property_Set](../../../core/libs/prx/libSceFreeType/src/ModuleProperties.cpp) - console signatures, property availability and backend/error equivalence are unverified. Only `truetype/interpreter-version`, the `cff`, `type1` and `t1cid` hinting-engine, random-seed, no-stem-darkening and darkening-parameters properties, and the autofitter fallback-script, default-script, no-stem-darkening and darkening-parameters properties follow bundled FreeType 2.13.3. Other module/property pairs throw, including properties carrying face pointers or callbacks. Values use 32-bit signed/unsigned integers, one-byte booleans or eight 32-bit darkening parameters; getters copy only successful native results. +- [FT_Property_Get, FT_Property_Set](../../../core/libs/prx/libSceFreeType/src/ModuleProperties.cpp) - console signatures, property availability and backend/error equivalence are unverified. The adapter follows bundled FreeType 2.13.3 under the existing LP64 assumption. Supported properties are `truetype/interpreter-version`, `cff`/`type1`/`t1cid` hinting-engine, random-seed, no-stem-darkening and darkening-parameters, and `autofitter` fallback-script, default-script, no-stem-darkening, darkening-parameters and increase-x-height. Values use 32-bit signed/unsigned integers, one-byte booleans, eight 32-bit darkening parameters, or the face+uint32 carrier whose getter updates only the limit on success. Other module/property pairs throw, including other properties carrying face pointers or callbacks. - [FT_Outline_Reverse, FT_Outline_Translate](../../../core/libs/prx/libSceFreeType/src/Outline.cpp) (libSceFreeType) - console signatures, count signedness and overflow behavior are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c) with LP64 coordinates. Translation uses 64-bit modular addition to match that build's `ADD_LONG`; reversal preserves each contour's first point and toggles the reverse-fill flag. Coordinates outside the host `FT_Pos` range still throw if the modified outline is later rendered on Windows From 1f8b81f3cd0cc4ceb7374540e50f52b68d56004f Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 28 Oct 2026 21:12:20 -0400 Subject: [PATCH 139/160] fix(libSceSaveData): read a save directory's time natively on Windows When a save has no stored mtime, its parameters fall back to the save directory's modification time. On Windows std::filesystem reads that through the UCRT stat call, which converts through local time using TZ, so with a TZ other than the Windows zone, including one libc applies from the guest environment, the reported time was off by the difference between the zones. The fallback now reads the time with GetFileAttributesExW. guest_savedata_search checks it under TZ values 12 hours either side of UTC. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libSceSaveData.native/Export.cpp | 17 +++++++++ core/libs/tests/GuestSaveDataSearch.cpp | 37 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 2caada7a85..5f4458136f 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -24,6 +24,15 @@ #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include "SaveData.hpp" #include "prx/libSceSaveData/SaveDataFile.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#endif static constexpr char SAVE_DIR[] = "_sd"; @@ -230,12 +239,20 @@ SaveDataParam load_param(const std::string& real_path) { SaveDataParam param{}; read_param_file(param_path(real_path), ¶m); if (param.mtime == 0) { +#ifdef _WIN32 + WIN32_FILE_ATTRIBUTE_DATA data{}; + if (GetFileAttributesExW(std::filesystem::path(real_path).c_str(), GetFileExInfoStandard, &data)) { + const auto ticks = static_cast((static_cast(data.ftLastWriteTime.dwHighDateTime) << 32) | data.ftLastWriteTime.dwLowDateTime); + param.mtime = (ticks - 116444736000000000LL) / 10000000LL; + } +#else std::error_code ec; const auto written = std::filesystem::last_write_time(real_path, ec); if (!ec) { const auto system = std::chrono::file_clock::to_sys(written); param.mtime = std::chrono::duration_cast(system.time_since_epoch()).count(); } +#endif } return param; } diff --git a/core/libs/tests/GuestSaveDataSearch.cpp b/core/libs/tests/GuestSaveDataSearch.cpp index 3cab6311ff..ccd56bc3ba 100644 --- a/core/libs/tests/GuestSaveDataSearch.cpp +++ b/core/libs/tests/GuestSaveDataSearch.cpp @@ -5,7 +5,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -303,6 +305,40 @@ static void BlockSize() { Require(info.blocks == 16384 && info.free_blocks == 16384, "a save must hold at most 16384 blocks"); } +static void SetHostZone(const char* zone) { +#ifdef _WIN32 + Require(_putenv_s("TZ", zone == nullptr ? "" : zone) == 0, "TZ could not be set"); + _tzset(); +#else + Require((zone == nullptr ? unsetenv("TZ") : setenv("TZ", zone, 1)) == 0, "TZ could not be set"); + tzset(); +#endif +} + +static void UnsetParamsReportDirectoryTime() { + std::filesystem::create_directories(std::filesystem::path("_sd") / "STAMP_A"); + const auto created = static_cast(std::time(nullptr)); + const char* inherited = std::getenv("TZ"); + const std::string previous = inherited == nullptr ? "" : inherited; + for (const char* zone : {"AAA-12", "AAA+12"}) { + SetHostZone(zone); + SceSaveDataDirName pattern{}; + std::snprintf(pattern.data, sizeof(pattern.data), "STAMP_A"); + SaveDataDirNameSearchCond cond{}; + cond.user_id = 1; + cond.dir_name = &pattern; + SceSaveDataDirName name{}; + SaveDataParam param{}; + SaveDataDirNameSearchResult result{}; + result.dir_names = &name; + result.dir_names_num = 1; + result.params = ¶m; + Require(sceSaveDataDirNameSearch(&cond, &result) == 0 && result.set_num == 1, "the unmounted save was not found"); + Require(param.mtime >= created - 60 && param.mtime <= created + 60, "a save without a stored mtime must report its directory's modification time under any TZ"); + } + SetHostZone(inherited == nullptr ? nullptr : previous.c_str()); +} + int main() { const auto previous = std::filesystem::current_path(); const auto root = std::filesystem::temp_directory_path() / @@ -317,6 +353,7 @@ int main() { TrailingWildcards(); SaveSize(); BlockSize(); + UnsetParamsReportDirectoryTime(); CheckNullArguments(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); From d092328a23ff8071e7354637b04fb06b6b8c859c Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Wed, 28 Oct 2026 20:15:28 -0500 Subject: [PATCH 140/160] test(libSceVoiceChat): cover offline requests and event handler operations --- core/libs/tests/GuestVoiceChat.cpp | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/core/libs/tests/GuestVoiceChat.cpp b/core/libs/tests/GuestVoiceChat.cpp index 7665ef4423..e4941b9977 100644 --- a/core/libs/tests/GuestVoiceChat.cpp +++ b/core/libs/tests/GuestVoiceChat.cpp @@ -19,18 +19,50 @@ int main() { Require(sceVoiceChatRegisterHandlers(nullptr, nullptr) == 0, "register handlers failed"); Require(sceVoiceChatRegisterMicEventHandler(0, nullptr, nullptr) == 0, "register mic event handler failed"); Require(sceVoiceChatRegisterChannelEventHandler(nullptr, nullptr) == 0, "register channel event handler failed"); + Require(sceVoiceChatRegisterDaemonEventHandler(nullptr, nullptr) == 0, "register daemon event handler failed"); + Require(sceVoiceChatRegisterMemberEventHandler(nullptr, nullptr) == 0, "register member event handler failed"); + Require(sceVoiceChatRegisterPartyUserStateChangedHandler(nullptr, nullptr) == 0, "register party user state changed handler failed"); + Require(sceVoiceChatRegisterVoiceEventHandler(nullptr, nullptr) == 0, "register voice event handler failed"); Require(sceVoiceChatUnregisterMicEventHandler(nullptr) == 0, "unregister mic event handler failed"); + Require(sceVoiceChatUnregisterChannelEventHandler(nullptr) == 0, "unregister channel event handler failed"); + Require(sceVoiceChatUnregisterDaemonEventHandler(nullptr) == 0, "unregister daemon event handler failed"); + Require(sceVoiceChatUnregisterMemberEventHandler(nullptr) == 0, "unregister member event handler failed"); + Require(sceVoiceChatUnregisterPartyUserStateChangedHandler(nullptr) == 0, "unregister party user state changed handler failed"); + Require(sceVoiceChatUnregisterVoiceEventHandler(nullptr) == 0, "unregister voice event handler failed"); Require(sceVoiceChatStartChannelEvent(nullptr) == 0, "start channel event failed"); Require(sceVoiceChatStopChannelEvent(nullptr) == 0, "stop channel event failed"); + Require(sceVoiceChatStartDaemonEvent(nullptr) == 0, "start daemon event failed"); + Require(sceVoiceChatStopDaemonEvent(nullptr) == 0, "stop daemon event failed"); + Require(sceVoiceChatStartMemberEvent(nullptr) == 0, "start member event failed"); + Require(sceVoiceChatStopMemberEvent(nullptr) == 0, "stop member event failed"); + Require(sceVoiceChatStartVoiceEvent(nullptr) == 0, "start voice event failed"); + Require(sceVoiceChatStopVoiceEvent(nullptr) == 0, "stop voice event failed"); Require(sceVoiceChatGetMicState(nullptr) == kSignedOut, "get mic state did not return signed out"); Require(sceVoiceChatGetUserVoiceVolumeGain(nullptr, nullptr) == kSignedOut, "get volume gain did not return signed out"); Require(sceVoiceChatSetUserVoiceVolumeGain(nullptr, 50) == kSignedOut, "set volume gain did not return signed out"); + Require(sceVoiceChatGetChannelMemberActiveChannelMatchingState(nullptr, nullptr) == kSignedOut, "matching state did not return signed out"); + Require(sceVoiceChatGetChannelMemberVoiceConnectionState(nullptr, nullptr) == kSignedOut, "voice connection state did not return signed out"); + Require(sceVoiceChatGetChatChannelMemberInfoList(nullptr, nullptr) == kSignedOut, "channel member list did not return signed out"); + Require(sceVoiceChatGetChatGroupMemberInfoList(nullptr, nullptr) == kSignedOut, "group member list did not return signed out"); Require(sceVoiceChatGetActiveChannel(nullptr) == kSignedOut, "get active channel did not return signed out"); Require(sceVoiceChatCreateRequest(nullptr, nullptr) == kSignedOut, "create request did not return signed out"); + Require(sceVoiceChatRequestRegisterSession(1, nullptr, nullptr) == kSignedOut, "register session did not return signed out"); + Require(sceVoiceChatRequestUnregisterSession(1, nullptr, nullptr) == kSignedOut, "unregister session did not return signed out"); + Require(sceVoiceChatRequestCreateGameSessionVoiceChatChannel(1, nullptr, nullptr) == kSignedOut, "create game session did not return signed out"); + Require(sceVoiceChatRequestDeleteGameSessionVoiceChatChannel(1, nullptr, nullptr) == kSignedOut, "delete game session did not return signed out"); + Require(sceVoiceChatRequestJoinGameSessionVoiceChatChannel(1, nullptr, nullptr) == kSignedOut, "join game session did not return signed out"); + Require(sceVoiceChatRequestLeaveGameSessionVoiceChatChannel(1, nullptr, nullptr) == kSignedOut, "leave game session did not return signed out"); + Require(sceVoiceChatRequestCreatePlayerSessionVoiceChatChannel(1, nullptr, nullptr) == kSignedOut, "create player session did not return signed out"); + Require(sceVoiceChatRequestDeletePlayerSessionVoiceChatChannel(1, nullptr, nullptr) == kSignedOut, "delete player session did not return signed out"); + Require(sceVoiceChatRequestCreateVoiceChatGroup(1, nullptr, nullptr) == kSignedOut, "create voice chat group did not return signed out"); + Require(sceVoiceChatRequestJoinVoiceChatGroup(1, nullptr, nullptr) == kSignedOut, "join voice chat group did not return signed out"); + Require(sceVoiceChatRequestLeaveVoiceChatGroup(1, nullptr, nullptr) == kSignedOut, "leave voice chat group did not return signed out"); + Require(sceVoiceChatRequestDeleteVoiceChatGroup(1, nullptr, nullptr) == kSignedOut, "delete voice chat group did not return signed out"); Require(sceVoiceChatRequestAcsTts(1, nullptr, nullptr) == kSignedOut, "tts request did not return signed out"); Require(sceVoiceChatAbortRequest(1) == 0, "abort request failed"); + Require(sceVoiceChatDeleteRequest(1) == 0, "delete request failed"); Require(sceVoiceChatTerminate() == 0, "termination failed"); std::puts("VoiceChat tests passed"); From efc22dcc12930de6c55aa111eb669ae04d68e88e Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Wed, 28 Oct 2026 20:17:10 -0500 Subject: [PATCH 141/160] test(ulobjmgr): cover NID aliases _sceUlobjmgrRegisterObject_nid_postfix and _sceUlobjmgrUnregisterObject_nid_postfix --- core/libs/tests/GuestUlobjmgr.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/libs/tests/GuestUlobjmgr.cpp b/core/libs/tests/GuestUlobjmgr.cpp index 3f6795a979..8a04790859 100644 --- a/core/libs/tests/GuestUlobjmgr.cpp +++ b/core/libs/tests/GuestUlobjmgr.cpp @@ -25,4 +25,18 @@ int main() { Require(_sceUlobjmgrUnregisterObject(0x3fff) == 0); Require(_sceUlobjmgrUnregisterObject(0x4000) == einval); Require(_sceUlobjmgrUnregisterObject(0xffffffffu) == einval); + + std::uint32_t nidId = 0xffffffffu; + Require(_sceUlobjmgrRegisterObject_nid_postfix(0, 1, &nidId) == einval); + Require(nidId == 0xffffffffu); + Require(_sceUlobjmgrRegisterObject_nid_postfix(0x1000, 0, &nidId) == einval); + Require(nidId == 0xffffffffu); + Require(_sceUlobjmgrRegisterObject_nid_postfix(0x1000, 1, nullptr) == einval); + Require(_sceUlobjmgrRegisterObject_nid_postfix(0x1000, -1, &nidId) == 0); + Require(nidId == 0); + + Require(_sceUlobjmgrUnregisterObject_nid_postfix(nidId) == 0); + Require(_sceUlobjmgrUnregisterObject_nid_postfix(0x3fff) == 0); + Require(_sceUlobjmgrUnregisterObject_nid_postfix(0x4000) == einval); + Require(_sceUlobjmgrUnregisterObject_nid_postfix(0xffffffffu) == einval); } From 9c13197689f404037e66028f3078b01ac46da239 Mon Sep 17 00:00:00 2001 From: dantdmdarealguy Date: Wed, 28 Oct 2026 20:55:04 -0500 Subject: [PATCH 142/160] docs(debt): narrow libSceSystemService's collapsed-dispatch note to the throwing calls The LncUtil collapsed dispatch entry, written by d4f2911b74 and 50c8103af2 (#9137), said its protected service implementations remain unchanged. Of the functions whose signatures d4f2911b74 restored, sceSystemServiceGetAppId answers for the running title since 7c83407e11 (#8354) and sceLncUtilGetAppId since 695041093b (#10801). Only the status, GPU-crash, dialog-activation, shared-memory, continue and VSH-ready calls still throw. --- docs/dev/debt/libSceSystemService.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 069cb2c0f5..f099e416b0 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -151,7 +151,7 @@ - [SystemService direct aliases](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): assembly-only jumps now share the confirmed signatures of their LncUtil/ShellCoreUtil targets for application-ID lists, remote-play/UI status, navigation, controller-focus permissions and boolean budget/status queries. Aliases of unresolved targets still require target or caller analysis. -- [LncUtil collapsed dispatch stubs](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): assembly table targets confirm application-ID lookup takes a title-ID string, current/focused status outputs occupy 64 bytes, GPU-crash status occupies 1320 bytes, and dialog activation writes one byte. Shared-memory registration takes a string. The protected service implementations remain unchanged; collapsed assembly procedures are not missing exports. +- [LncUtil collapsed dispatch stubs](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): assembly table targets confirm application-ID lookup takes a title-ID string, current/focused status outputs occupy 64 bytes, GPU-crash status occupies 1320 bytes, and dialog activation writes one byte. Shared-memory registration takes a string. `sceLncUtilGetAppStatus`, `sceLncUtilGetAppFocusedAppStatus`, `sceLncUtilGetGpuCrashFullDumpAppStatus`, `sceLncUtilIsCdlgActivated`, `sceLncUtilRegisterCdlgSharedMemoryName`, `sceLncUtilUnregisterCdlgSharedMemoryName`, `sceLncUtilContinueApp` and `sceLncUtilNotifyVshReady` still throw; collapsed assembly procedures are not missing exports. - [SystemService launch and telemetry arguments](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): store-launch parameters contain only a four-byte size; web-app launch takes one string argument. Process lifecycle wrappers forward the verified LncUtil argument lists. TelemetrySetData takes a string key, binary payload and machine-sized length; TelemetrySetData2 takes two strings. Token storage consumes a 32-bit identifier and string; only its signature is restored, with no token values included. From ecd5ae72f78c6e9f9a41c7c77c412e9fe947b6e5 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Thu, 29 Oct 2026 00:30:58 +0100 Subject: [PATCH 143/160] docs(agc): count how local titles call sceAgcAcbMemSemaphore and sceAgcDcbMemSemaphore Extends the entry by one sentence of counts: 13 of 53 local titles call both functions once each, from one wrapper in the title's library code that passes the operation as a byte and zeroes two arguments for operation 7. Counts and a description of the title code only; nothing is claimed about console behavior. Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceAgc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 69ae63412b..fa94b3e26e 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -24,7 +24,7 @@ - [sceAgcCreateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - name from its NID (`dbOlWdppb4o`); same arguments as `sceAgcCreateInterpolantMapping_0100`; PPSA26344 passes three pointers at each of nine observed call sites (`0x9F914E`, `0x9F91E2`, `0x9F9216`, `0x1604BBD`, `0x16122D7`, `0x1618285`, `0x161838C`, `0x16184EE`, `0x1618607`), consistent with that argument assumption, but does not read the return value there, so its return type is not established. - [sceAgcGetDataPacketPayloadRange](../../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - signature, the `{base, size in bytes}` output and the type 0 range (header + 1, one dword longer than the type 1 payload) from KytyPS5 only, matching `sceAgcGetDataPacketPayloadAddress_0090`; prosper returns header + 2 for both types from that address function - [sceAgcGetDataPacketPayloadAddress](../../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - forward to `sceAgcGetDataPacketPayloadAddress_0090` and the inferred (addr, cmd, type) parameters follow SharpProspero and decrypted firmware 3.20 NID mapping; not verified on console hardware -- [sceAgcAcbMemSemaphore, sceAgcDcbMemSemaphore](../../../core/libs/prx/libSceAgc/Acb/src/Sync.cpp) (libSceAgc) - signature (cb, semaphoreAddr, waitForMailbox, signalType, operation) inferred from PPSA03396, whose wrapper passes the address second and operation 6 (signal) or 7 (wait) last; the three trailing arguments go to the packet as-is +- [sceAgcAcbMemSemaphore, sceAgcDcbMemSemaphore](../../../core/libs/prx/libSceAgc/Acb/src/Sync.cpp) (libSceAgc) - signature (cb, semaphoreAddr, waitForMailbox, signalType, operation) inferred from PPSA03396, whose wrapper passes the address second and operation 6 (signal) or 7 (wait) last; the three trailing arguments go to the packet as-is Of 53 local titles, 13 call both functions, each once (26 call sites), from a wrapper in the title's library code that is the same in all 13: it takes the operation as an 8-bit third parameter, passes 0 for waitForMailbox and signalType when the operation is 7, and does not read the result. The constant operation values are not visible at these call sites. - [sceAgcCbMemSemaphore](../../../core/libs/prx/libSceAgc/Cb/src/Sync.cpp) (libSceAgc) - signature (cb, semaphoreAddr, operation, modifier, value) follows SharpProspero; writes 4-dword PM4 0x39 packet with control word 0x80100000 following console oracle measurements in prosper (cases agcbuild_CbMemSemaphore_a/b/c); operation, modifier and value are unused - [sceAgcAsyncCondExecPatchSetCommandAddress, sceAgcAsyncCondExecPatchSetEnd](../../../core/libs/prx/libSceAgc/Patch/src/CondExec.cpp) (libSceAgc) - no reference has a body for them; they are assumed to patch the COND_EXEC packet like `sceAgcCondExecPatchSetCommandAddress` and `sceAgcCondExecPatchSetEnd`, since `sceAgcAcbCondExec` writes the same packet as `sceAgcDcbCondExec`, as `sceAgcAsyncRewindPatchSetRewindState` does for REWIND - [sceAgcAcbAtomicGds_0900](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - name from its NID (`gQkqkLttcpw`) From df6354b0a80d9fa0bc7b3c7dda5386c480fd0743 Mon Sep 17 00:00:00 2001 From: dantdmdarealguy Date: Wed, 28 Oct 2026 21:19:30 -0500 Subject: [PATCH 144/160] docs(usage): drop the Linux execute permission sentence from --autorun The relinker writes every Linux and macOS output with execute permission since ab94ee21d7, as the running section already says, so the --autorun row no longer attributes it to --autorun. --- docs/user/USAGE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 4e3f86ce56..ada5592fe2 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -80,7 +80,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `unused-filter=2` | Apply strict unused-import analysis and compact the PLT. Unsupported analysis cases cause an error. | | `--registry` | Write `.registry.json` beside the output executable, and `..registry.json` for each converted bundled module. | | `--rpath ` | Set the system library search path. Quote `$ORIGIN` to prevent shell expansion, for example `--rpath '$ORIGIN/libs'` in Bash or PowerShell. On Linux every colon-separated entry must be nonempty, and with guest modules each must be absolute or begin with `$ORIGIN`. Windows requires a nonempty ASCII path and supports `$ORIGIN` as the executable directory. | -| `--autorun` | Run the output after conversion, print its exit code, and wait for Enter. Adds executable permissions for Linux output. Requires the target OS and prepared runtime layout. | +| `--autorun` | Run the output after conversion, print its exit code, and wait for Enter. Requires the target OS and prepared runtime layout. | | `--sce-module-path ` | Parent of the module directories, not the module directory itself. Default: input ELF parent; relative input paths resolve from the process working directory. Explicit absolute path: used directly. Explicit relative path (including `.` and `..`): relative to the process working directory. Output path has no effect. Requires one nonempty value; repeats, nonexistent or nondirectory paths, access failures, and combination with `--skip-sce-module` are errors. | | `--module-dir ` | Relink the ELF modules of a directory under the module parent directory, besides `sce_module`/`sce_modules`/`prx`; outputs keep the relative path. Needed modules are mapped at startup. On Linux, modules in these extra directories initialize only when explicitly loaded, with the arguments from `sceKernelLoadStartModule` (for example Unity plugins in `Media/Plugins`). A module another module needs initializes at startup on Windows and macOS; one only the executable needs initializes when explicitly loaded on Windows and at startup on macOS. Other modules initialize when loaded. Repeatable; `.` is the parent itself, without the input ELF. Conflicts with `--skip-sce-module`. | | `--executable ` | Also convert another executable of the title, which it starts with `sceSystemServiceLoadExec`; the path is relative to the module parent directory, for example `picker/picker.bin`. It is written beside the output, named after the path with `/` replaced by `.` and given the output's extension (`picker.picker.bin.exe`), and uses the same `libs/`, `app0/` and guest modules. It is an error if a guest module converts differently for it. Repeatable. Conflicts with `--skip-sce-module`. | From 54a437655c5c0d165b2ce7c8adf627f19488f17d Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 28 Oct 2026 22:52:57 -0400 Subject: [PATCH 145/160] docs: state that bundled modules are searched before the host libraries An import binds to the first module that exports its NID. The Windows entry stub searches converted app0 modules before libs, and on Linux the guest modules loaded at startup come before the host libraries in DT_NEEDED. A title that ships libc.prx therefore runs its own libc for the NIDs that module exports. Co-Authored-By: Claude Opus 5.5 --- docs/dev/ARCHITECTURE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 57d0346c15..befee817f9 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -44,6 +44,7 @@ flowchart LR - [`core/relinker/main.cpp`](../../core/relinker/main.cpp) runs the steps in this order. `--to-intel` is optional, see [USAGE.md](../user/USAGE.md). - Each library in [`core/libs/prx`](../../core/libs/prx) builds as a shared library. After the build, `nid_patcher` ([`core/libs/nid`](../../core/libs/nid)) renames every export to its NID, computed from the function name. `APS5_EXPORT("", func)` sets the NID directly when the name is unknown. Every other library keeps the names libc exports; it reads them from `libc.exports`, which `nid_patcher --write-export-list` rewrites only when that list changes. On Windows the link writes each import library with `SOURCE_DATE_EPOCH=0` and `ranlib -D` and replaces it only when its bytes change. A source edit that keeps the exports therefore relinks and re-patches only its own library. +- An import binds to the first module that exports its NID, and bundled modules come first. On Windows the entry stub ([`WindowsEntryStubBuilder.cpp`](../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp)) searches the converted `app0` modules before `libs`, and adds the host `libc.prx` to the search of every other host import. On Linux [`GuestModuleBuilder.cpp`](../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) lists the guest modules loaded at startup before the host libraries in the executable's `DT_NEEDED`. A title that ships its own `libc.prx`, `libSceJobManager.prx` or other module therefore runs that module's code for every NID it exports; the host library only serves the NIDs it lacks, and its `libSceLibcInternal` exports. ## Main-module entry registers From 77642c6a26b71cbdb14538816e2436d257e9517c Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 28 Oct 2026 23:18:11 -0400 Subject: [PATCH 146/160] docs: scope the module search order and its libSceLibcInternal exception Follows rooklz's review. The bullet now covers Windows and Linux only, says the Windows stub looks up by symbol name before NID, and states that Windows searches the host libSceLibcInternal.prx and libc.prx before the bundled libc.prx for libSceLibcInternal imports, skipping its exports that only return zero. Co-Authored-By: Claude Opus 5.5 --- docs/dev/ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index befee817f9..6becee627b 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -44,7 +44,7 @@ flowchart LR - [`core/relinker/main.cpp`](../../core/relinker/main.cpp) runs the steps in this order. `--to-intel` is optional, see [USAGE.md](../user/USAGE.md). - Each library in [`core/libs/prx`](../../core/libs/prx) builds as a shared library. After the build, `nid_patcher` ([`core/libs/nid`](../../core/libs/nid)) renames every export to its NID, computed from the function name. `APS5_EXPORT("", func)` sets the NID directly when the name is unknown. Every other library keeps the names libc exports; it reads them from `libc.exports`, which `nid_patcher --write-export-list` rewrites only when that list changes. On Windows the link writes each import library with `SOURCE_DATE_EPOCH=0` and `ranlib -D` and replaces it only when its bytes change. A source edit that keeps the exports therefore relinks and re-patches only its own library. -- An import binds to the first module that exports its NID, and bundled modules come first. On Windows the entry stub ([`WindowsEntryStubBuilder.cpp`](../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp)) searches the converted `app0` modules before `libs`, and adds the host `libc.prx` to the search of every other host import. On Linux [`GuestModuleBuilder.cpp`](../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) lists the guest modules loaded at startup before the host libraries in the executable's `DT_NEEDED`. A title that ships its own `libc.prx`, `libSceJobManager.prx` or other module therefore runs that module's code for every NID it exports; the host library only serves the NIDs it lacks, and its `libSceLibcInternal` exports. +- On Windows and Linux, bundled modules come before AnyPS5's libraries when an import is looked up. On Windows the entry stub ([`WindowsEntryStubBuilder.cpp`](../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp)) tries the converted `app0` modules before `libs`, by symbol name first and then by NID, and takes the first module that exports it; it also adds the host `libc.prx` to the search of every other host import. Imports from `libSceLibcInternal.prx` are the exception: the stub tries the host `libSceLibcInternal.prx` and `libc.prx` first and the bundled `libc.prx` last, skipping its exports that only return zero. On Linux [`GuestModuleBuilder.cpp`](../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) lists the guest modules loaded at startup before every host library in the executable's `DT_NEEDED`, `libSceLibcInternal.prx` included. A title that ships its own `libc.prx`, `libSceJobManager.prx` or other module therefore runs that module's code for the NIDs it exports, and the host library serves the NIDs it lacks. ## Main-module entry registers From ae4fbac23618a95270534632db7fde6d8dcb7515 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Thu, 29 Oct 2026 04:29:37 +0100 Subject: [PATCH 147/160] docs(debt): count the flags local titles pass to sceNetSend and sceNetSendto Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libSceNet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index f54727a850..de1fda83da 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -58,7 +58,7 @@ - [sceNetSocketClose, sceNetEpollControl and sceNetEpollWait](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - close and concurrent registration ordering are unmeasured on a console. Closed descriptors retire their registrations, and a wait discards snapshots invalidated by close, DEL or MOD; descriptor identity follows [Linux epoll(7)](https://man7.org/linux/man-pages/man7/epoll.7.html). ADD, MOD and DEL reject closed or invalid socket descriptors with `EBADF`, as [Linux epoll_ctl(2)](https://man7.org/linux/man-pages/man2/epoll_ctl.2.html) does; this error ordering is unmeasured on a console. Duplicate-descriptor alias lifetime is not modeled by this epoll backend. - [sceNetEpollControl and sceNetEpollWait](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) take sockets made with libkernel's `socket()`. PPSA02429 registers such sockets and retries the failed `ADD` and `DEL` about 1.6 million times in its first seconds. This follows the position taken for PPSA30839 in `sceNetSocketClose` and `sceNetSetsockopt`, that both libraries work on the same kernel sockets; epoll on them is not measured on a console. libkernel's `close()` and `sceKernelClose` retire their registrations at once, so a socket that reuses the number starts unregistered. - [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - console ordering between competing readers and writers is unmeasured. The per-call behavior follows FreeBSD 11.4's `soreceive_generic` and `sosend_generic` (`sys/kern/uipc_socket.c`); `0x80` is FreeBSD's flag value (`sys/sys/socket.h`). On Windows native sockets stay non-blocking. `MSG_DONTWAIT` and guest `SO_NBIO` return one host transfer's result. Blocking stream sends queue the whole buffer with one deadline and return progress after a timeout or host error. SSL uses this path through the native owner, retaining the socket, its mode and timeouts. Readiness waits poll at most every 10 ms for local close, so idle blocked operations wake periodically. -- [MSG_NOSIGNAL](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - `sceNetSend`, `sceNetSendto` and `sceNetSendmsg` accept `0x20000`, FreeBSD's `MSG_NOSIGNAL` (`sys/sys/socket.h`), which ProsperoTV (PPSA99003) passes on every phone-remote reply. Firmware 13.42's `libSceNet.sprx` adds `0x40020000` to the flags of every send and refuses no flag itself ([measured in an emulator](https://github.com/boykopovar/AnyPS5/pull/11037#issuecomment-6400759240)), so passing the flag changes nothing on the console. What `0x40000000` means and which flags the kernel refuses are unknown; other flags still fail with `EOPNOTSUPP` here. Host sends outside Windows already pass `MSG_NOSIGNAL` and Windows raises no `SIGPIPE`, so a send on a stream whose write side is shut down fails with `EPIPE` +- [MSG_NOSIGNAL](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - `sceNetSend`, `sceNetSendto` and `sceNetSendmsg` accept `0x20000`, FreeBSD's `MSG_NOSIGNAL` (`sys/sys/socket.h`), which ProsperoTV (PPSA99003) passes on every phone-remote reply. Firmware 13.42's `libSceNet.sprx` adds `0x40020000` to the flags of every send and refuses no flag itself ([measured in an emulator](https://github.com/boykopovar/AnyPS5/pull/11037#issuecomment-6400759240)), so passing the flag changes nothing on the console. What `0x40000000` means and which flags the kernel refuses are unknown; other flags still fail with `EOPNOTSUPP` here. Host sends outside Windows already pass `MSG_NOSIGNAL` and Windows raises no `SIGPIPE`, so a send on a stream whose write side is shut down fails with `EPIPE`. Of 53 local titles, 35 call `sceNetSend` or `sceNetSendto`; in their executables (libraries, 2 tail jumps unread) flags are 0 at 86 call sites, 0x80 at 10, non-constant at 2, never 0x40000000 or 0x20000. - [Stream sends on macOS](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - a `MSG_DONTWAIT` transfer also passes XNU's `MSG_NBIO` (`0x20000`, absent from the SDK headers), because XNU's `MSG_DONTWAIT` does not keep a stream send from waiting for buffer space. A blocking stream send goes to the host in sends of 64 KiB, so an error after some progress, such as an abort, reports the bytes already queued, which one host send would drop; the bytes that the failing 64 KiB send had queued are not counted - [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) on a datagram socket with no local address wait on Windows and macOS until a bind, connect or send gives it one, checking the host socket every 10 ms. Winsock fails such a receive with `WSAEINVAL`, where FreeBSD 11.4's `soreceive_dgram` waits for a datagram (`sys/kern/uipc_socket.c`). On macOS the host receive waits, but a receive-side `shutdown` does not wake it. A non-blocking socket fails with `EAGAIN` at once, and the receive timeout ends the wait with `EAGAIN`; when the address comes first, the call waits for a datagram only for what is left of the timeout. A `sceNetShutdown` of the receive side ends the wait and the receive returns 0, and a zero-length receive returns 0 without waiting, as `soreceive_dgram` does when the socket can't receive more or nothing was asked for. An abort or close ends the wait, and the host receive then fails as it did before - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does From 1c15869a4e77bec6c4e173b0a5364e44c438bc3d Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 28 Oct 2026 23:30:19 -0400 Subject: [PATCH 148/160] docs: limit the bundled-module rule to imports that name the library Follows oneandonlydean's review: on Windows a converted app0 module is put ahead of the host library only for imports that name its library, so a bundled module serves those imports, not every NID it exports. Co-Authored-By: Claude Opus 5.5 --- docs/dev/ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 6becee627b..d8f94391b1 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -44,7 +44,7 @@ flowchart LR - [`core/relinker/main.cpp`](../../core/relinker/main.cpp) runs the steps in this order. `--to-intel` is optional, see [USAGE.md](../user/USAGE.md). - Each library in [`core/libs/prx`](../../core/libs/prx) builds as a shared library. After the build, `nid_patcher` ([`core/libs/nid`](../../core/libs/nid)) renames every export to its NID, computed from the function name. `APS5_EXPORT("", func)` sets the NID directly when the name is unknown. Every other library keeps the names libc exports; it reads them from `libc.exports`, which `nid_patcher --write-export-list` rewrites only when that list changes. On Windows the link writes each import library with `SOURCE_DATE_EPOCH=0` and `ranlib -D` and replaces it only when its bytes change. A source edit that keeps the exports therefore relinks and re-patches only its own library. -- On Windows and Linux, bundled modules come before AnyPS5's libraries when an import is looked up. On Windows the entry stub ([`WindowsEntryStubBuilder.cpp`](../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp)) tries the converted `app0` modules before `libs`, by symbol name first and then by NID, and takes the first module that exports it; it also adds the host `libc.prx` to the search of every other host import. Imports from `libSceLibcInternal.prx` are the exception: the stub tries the host `libSceLibcInternal.prx` and `libc.prx` first and the bundled `libc.prx` last, skipping its exports that only return zero. On Linux [`GuestModuleBuilder.cpp`](../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) lists the guest modules loaded at startup before every host library in the executable's `DT_NEEDED`, `libSceLibcInternal.prx` included. A title that ships its own `libc.prx`, `libSceJobManager.prx` or other module therefore runs that module's code for the NIDs it exports, and the host library serves the NIDs it lacks. +- On Windows and Linux, bundled modules come before AnyPS5's libraries when an import is looked up. On Windows the entry stub ([`WindowsEntryStubBuilder.cpp`](../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp)) tries the converted `app0` modules before `libs`, by symbol name first and then by NID, and takes the first module that exports it; it also adds the host `libc.prx` to the search of every other host import. Imports from `libSceLibcInternal.prx` are the exception: the stub tries the host `libSceLibcInternal.prx` and `libc.prx` first and the bundled `libc.prx` last, skipping its exports that only return zero. On Linux [`GuestModuleBuilder.cpp`](../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) lists the guest modules loaded at startup before every host library in the executable's `DT_NEEDED`, `libSceLibcInternal.prx` included. A title that ships its own `libc.prx`, `libSceJobManager.prx` or other module therefore runs that module's code for the imports that name its library and that it exports, and the host library serves the rest. ## Main-module entry registers From 1a47f9f0eeb214588eccf6c813a80ff1e2190ca0 Mon Sep 17 00:00:00 2001 From: DeveshB-1 Date: Thu, 29 Oct 2026 09:15:45 +0530 Subject: [PATCH 149/160] fix(libkernel): return EFAULT from ioctl FIONREAD for an unwritable argument --- core/libs/prx/libkernel/File/src/DescriptorControl.cpp | 5 +---- core/libs/tests/GuestKernelFcntl.cpp | 4 ++++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index 3aea1bdb28..63828320b8 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -576,10 +576,7 @@ int Ioctl(int fd, std::uint64_t request, void* argument) { if (*kind != Kind::File && *kind != Kind::Pipe && *kind != Kind::Socket && *kind != Kind::ArchivedFile) Unsupported("ioctl", "FIONREAD", *kind, fd); const auto archived = File::ArchivedFileRemaining(fd); const int available = archived ? static_cast(std::min(*archived, std::numeric_limits::max())) : Readable(fd, *kind, "ioctl"); - if (argument == nullptr) return Fail(GuestEfault); - const GuestArena::HostWrite destination(argument, sizeof(available)); - if (!destination.Open()) return Fail(GuestEfault); - std::memcpy(argument, &available, sizeof(available)); + if (!GuestCopyOut_nid_no_patch(argument, &available, sizeof(available))) return Fail(GuestEfault); return 0; } if (((command >> 8) & 0xff) == TerminalGroup) return Fail(GuestEnotty); diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index 202503bc2a..d625cec04d 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -258,6 +258,8 @@ static void FileFlagsRoundTrip() { RequireMalformedRequestsFail(file); RequireFailure(ioctl_nid_postfix(file, NonblockingRequest, nullptr), GuestEfault); RequireFailure(ioctl_nid_postfix(file, NonblockingRequest, reinterpret_cast(std::uintptr_t{0x10})), GuestEfault); + RequireFailure(ioctl_nid_postfix(file, ReadableRequest, nullptr), GuestEfault); + RequireFailure(ioctl_nid_postfix(file, ReadableRequest, reinterpret_cast(std::uintptr_t{0x10})), GuestEfault); Require(ThrowsWith([file] { int on = 1; ioctl_nid_postfix(file, AsyncRequest, &on); }, "ioctl: request 0x8004667D on a file is not implemented")); RequireFailure(fcntl_nid_postfix(file, DuplicateCommand, -1), GuestEinval); @@ -474,6 +476,8 @@ static void PipeFlagsRoundTrip() { int readable = -1; Require(ioctl_nid_postfix(pipe[0], ReadableRequest, &readable) == 0 && readable == 3); Require(ioctl_nid_postfix(pipe[1], ReadableRequest, &readable) == 0 && readable == 0); + RequireFailure(ioctl_nid_postfix(pipe[0], ReadableRequest, nullptr), GuestEfault); + RequireFailure(ioctl_nid_postfix(pipe[0], ReadableRequest, reinterpret_cast(std::uintptr_t{0x10})), GuestEfault); Require(read_nid_postfix(pipe[0], buffer, sizeof(buffer)) == 3); RequireFailure(static_cast(read_nid_postfix(pipe[0], buffer, sizeof(buffer))), GuestEagain); From 44b24ddb5c2ad1293f8bab29a9ce5293d7bf8b49 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Thu, 29 Oct 2026 04:55:01 +0100 Subject: [PATCH 150/160] docs(agc): say that the MemSemaphore counts are direct calls from the executable Adds the missing full stop after the original sentence and states that the 13 titles call both functions directly from the executable, so the entry does not read as if the other titles never call them. Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceAgc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index fa94b3e26e..dd8572eb71 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -24,7 +24,7 @@ - [sceAgcCreateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - name from its NID (`dbOlWdppb4o`); same arguments as `sceAgcCreateInterpolantMapping_0100`; PPSA26344 passes three pointers at each of nine observed call sites (`0x9F914E`, `0x9F91E2`, `0x9F9216`, `0x1604BBD`, `0x16122D7`, `0x1618285`, `0x161838C`, `0x16184EE`, `0x1618607`), consistent with that argument assumption, but does not read the return value there, so its return type is not established. - [sceAgcGetDataPacketPayloadRange](../../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - signature, the `{base, size in bytes}` output and the type 0 range (header + 1, one dword longer than the type 1 payload) from KytyPS5 only, matching `sceAgcGetDataPacketPayloadAddress_0090`; prosper returns header + 2 for both types from that address function - [sceAgcGetDataPacketPayloadAddress](../../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - forward to `sceAgcGetDataPacketPayloadAddress_0090` and the inferred (addr, cmd, type) parameters follow SharpProspero and decrypted firmware 3.20 NID mapping; not verified on console hardware -- [sceAgcAcbMemSemaphore, sceAgcDcbMemSemaphore](../../../core/libs/prx/libSceAgc/Acb/src/Sync.cpp) (libSceAgc) - signature (cb, semaphoreAddr, waitForMailbox, signalType, operation) inferred from PPSA03396, whose wrapper passes the address second and operation 6 (signal) or 7 (wait) last; the three trailing arguments go to the packet as-is Of 53 local titles, 13 call both functions, each once (26 call sites), from a wrapper in the title's library code that is the same in all 13: it takes the operation as an 8-bit third parameter, passes 0 for waitForMailbox and signalType when the operation is 7, and does not read the result. The constant operation values are not visible at these call sites. +- [sceAgcAcbMemSemaphore, sceAgcDcbMemSemaphore](../../../core/libs/prx/libSceAgc/Acb/src/Sync.cpp) (libSceAgc) - signature (cb, semaphoreAddr, waitForMailbox, signalType, operation) inferred from PPSA03396, whose wrapper passes the address second and operation 6 (signal) or 7 (wait) last; the three trailing arguments go to the packet as-is. Of 53 local titles, 13 call both functions directly from the executable, each once (26 call sites), from a wrapper in the title's library code that is the same in all 13: it takes the operation as an 8-bit third parameter, passes 0 for waitForMailbox and signalType when the operation is 7, and does not read the result. The constant operation values are not visible at these call sites. - [sceAgcCbMemSemaphore](../../../core/libs/prx/libSceAgc/Cb/src/Sync.cpp) (libSceAgc) - signature (cb, semaphoreAddr, operation, modifier, value) follows SharpProspero; writes 4-dword PM4 0x39 packet with control word 0x80100000 following console oracle measurements in prosper (cases agcbuild_CbMemSemaphore_a/b/c); operation, modifier and value are unused - [sceAgcAsyncCondExecPatchSetCommandAddress, sceAgcAsyncCondExecPatchSetEnd](../../../core/libs/prx/libSceAgc/Patch/src/CondExec.cpp) (libSceAgc) - no reference has a body for them; they are assumed to patch the COND_EXEC packet like `sceAgcCondExecPatchSetCommandAddress` and `sceAgcCondExecPatchSetEnd`, since `sceAgcAcbCondExec` writes the same packet as `sceAgcDcbCondExec`, as `sceAgcAsyncRewindPatchSetRewindState` does for REWIND - [sceAgcAcbAtomicGds_0900](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - name from its NID (`gQkqkLttcpw`) From ecaec7b2e6a30409a8e54507a2c2140693a9094e Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Thu, 29 Oct 2026 04:58:52 +0100 Subject: [PATCH 151/160] docs(debt): state that the send flags count reads only the executables Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libSceNet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index de1fda83da..a9da20ebd8 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -58,7 +58,7 @@ - [sceNetSocketClose, sceNetEpollControl and sceNetEpollWait](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - close and concurrent registration ordering are unmeasured on a console. Closed descriptors retire their registrations, and a wait discards snapshots invalidated by close, DEL or MOD; descriptor identity follows [Linux epoll(7)](https://man7.org/linux/man-pages/man7/epoll.7.html). ADD, MOD and DEL reject closed or invalid socket descriptors with `EBADF`, as [Linux epoll_ctl(2)](https://man7.org/linux/man-pages/man2/epoll_ctl.2.html) does; this error ordering is unmeasured on a console. Duplicate-descriptor alias lifetime is not modeled by this epoll backend. - [sceNetEpollControl and sceNetEpollWait](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) take sockets made with libkernel's `socket()`. PPSA02429 registers such sockets and retries the failed `ADD` and `DEL` about 1.6 million times in its first seconds. This follows the position taken for PPSA30839 in `sceNetSocketClose` and `sceNetSetsockopt`, that both libraries work on the same kernel sockets; epoll on them is not measured on a console. libkernel's `close()` and `sceKernelClose` retire their registrations at once, so a socket that reuses the number starts unregistered. - [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - console ordering between competing readers and writers is unmeasured. The per-call behavior follows FreeBSD 11.4's `soreceive_generic` and `sosend_generic` (`sys/kern/uipc_socket.c`); `0x80` is FreeBSD's flag value (`sys/sys/socket.h`). On Windows native sockets stay non-blocking. `MSG_DONTWAIT` and guest `SO_NBIO` return one host transfer's result. Blocking stream sends queue the whole buffer with one deadline and return progress after a timeout or host error. SSL uses this path through the native owner, retaining the socket, its mode and timeouts. Readiness waits poll at most every 10 ms for local close, so idle blocked operations wake periodically. -- [MSG_NOSIGNAL](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - `sceNetSend`, `sceNetSendto` and `sceNetSendmsg` accept `0x20000`, FreeBSD's `MSG_NOSIGNAL` (`sys/sys/socket.h`), which ProsperoTV (PPSA99003) passes on every phone-remote reply. Firmware 13.42's `libSceNet.sprx` adds `0x40020000` to the flags of every send and refuses no flag itself ([measured in an emulator](https://github.com/boykopovar/AnyPS5/pull/11037#issuecomment-6400759240)), so passing the flag changes nothing on the console. What `0x40000000` means and which flags the kernel refuses are unknown; other flags still fail with `EOPNOTSUPP` here. Host sends outside Windows already pass `MSG_NOSIGNAL` and Windows raises no `SIGPIPE`, so a send on a stream whose write side is shut down fails with `EPIPE`. Of 53 local titles, 35 call `sceNetSend` or `sceNetSendto`; in their executables (libraries, 2 tail jumps unread) flags are 0 at 86 call sites, 0x80 at 10, non-constant at 2, never 0x40000000 or 0x20000. +- [MSG_NOSIGNAL](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - `sceNetSend`, `sceNetSendto` and `sceNetSendmsg` accept `0x20000`, FreeBSD's `MSG_NOSIGNAL` (`sys/sys/socket.h`), which ProsperoTV (PPSA99003) passes on every phone-remote reply. Firmware 13.42's `libSceNet.sprx` adds `0x40020000` to the flags of every send and refuses no flag itself ([measured in an emulator](https://github.com/boykopovar/AnyPS5/pull/11037#issuecomment-6400759240)), so passing the flag changes nothing on the console. What `0x40000000` means and which flags the kernel refuses are unknown; other flags still fail with `EOPNOTSUPP` here. Host sends outside Windows already pass `MSG_NOSIGNAL` and Windows raises no `SIGPIPE`, so a send on a stream whose write side is shut down fails with `EPIPE`. Of 53 local titles, 35 call `sceNetSend` or `sceNetSendto` directly from their executables (2 tail jumps unread): flags 0 at 86 call sites, 0x80 at 10, non-constant at 2, never 0x40000000 or 0x20000. - [Stream sends on macOS](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - a `MSG_DONTWAIT` transfer also passes XNU's `MSG_NBIO` (`0x20000`, absent from the SDK headers), because XNU's `MSG_DONTWAIT` does not keep a stream send from waiting for buffer space. A blocking stream send goes to the host in sends of 64 KiB, so an error after some progress, such as an abort, reports the bytes already queued, which one host send would drop; the bytes that the failing 64 KiB send had queued are not counted - [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) on a datagram socket with no local address wait on Windows and macOS until a bind, connect or send gives it one, checking the host socket every 10 ms. Winsock fails such a receive with `WSAEINVAL`, where FreeBSD 11.4's `soreceive_dgram` waits for a datagram (`sys/kern/uipc_socket.c`). On macOS the host receive waits, but a receive-side `shutdown` does not wake it. A non-blocking socket fails with `EAGAIN` at once, and the receive timeout ends the wait with `EAGAIN`; when the address comes first, the call waits for a datagram only for what is left of the timeout. A `sceNetShutdown` of the receive side ends the wait and the receive returns 0, and a zero-length receive returns 0 without waiting, as `soreceive_dgram` does when the socket can't receive more or nothing was asked for. An abort or close ends the wait, and the host receive then fails as it did before - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does From 1c82f5629614c10ec35998ca87f2ce72d09bfdcf Mon Sep 17 00:00:00 2001 From: XairRrR Date: Thu, 29 Oct 2026 05:23:11 +0100 Subject: [PATCH 152/160] docs(environment): list _wgetenv among the readers of environment names The opening paragraph of docs/dev/ENVIRONMENT.md lists the calls through which the code reads environment names, and so the reads that the Conventions check holds to a row. It omits _wgetenv with a wide literal. tools/env_reference.py has recognised that form since 65da8ac106, and the code reads nine names through it on Windows, among them ANYPS5_SHADER_CACHE_DIR, ANYPS5_SCREENSHOTS and APS5_DUMP_SUBMITS. The paragraph now names _wgetenv beside GetEnvironmentVariableW. AI-assisted: yes --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 05678c738d..d3d2b90d19 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -1,6 +1,6 @@ # Environment variables -The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetting`, `MaskFromEnvironment`, `EnvInt`, `GetEnvironmentVariableA`, `GetEnvironmentVariableW` with a wide literal, `configured`, `PipelineSwitch`, `environmentCount`, `EnvironmentNumber` and `addWideString` (the wide names the Windows entry stub reads). This table lists every `APS5_*` and `ANYPS5_*` name it reads, its kind, the value used when it is unset and what it does, one table per library. Regenerate the names with `python3 tools/env_reference.py --table`; the Conventions check fails when a name the code reads has no row here, or a row's name is no longer read. +The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetting`, `MaskFromEnvironment`, `EnvInt`, `GetEnvironmentVariableA`, `GetEnvironmentVariableW` and `_wgetenv` with a wide literal, `configured`, `PipelineSwitch`, `environmentCount`, `EnvironmentNumber` and `addWideString` (the wide names the Windows entry stub reads). This table lists every `APS5_*` and `ANYPS5_*` name it reads, its kind, the value used when it is unset and what it does, one table per library. Regenerate the names with `python3 tools/env_reference.py --table`; the Conventions check fails when a name the code reads has no row here, or a row's name is no longer read. ### core/libs From f744a54063ad4f4acc69c05f3d154d9d067d569e Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Thu, 29 Oct 2026 05:32:41 +0100 Subject: [PATCH 153/160] docs(debt): name what the send flags count leaves unread Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libSceNet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index a9da20ebd8..fc0744f1cf 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -58,7 +58,7 @@ - [sceNetSocketClose, sceNetEpollControl and sceNetEpollWait](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - close and concurrent registration ordering are unmeasured on a console. Closed descriptors retire their registrations, and a wait discards snapshots invalidated by close, DEL or MOD; descriptor identity follows [Linux epoll(7)](https://man7.org/linux/man-pages/man7/epoll.7.html). ADD, MOD and DEL reject closed or invalid socket descriptors with `EBADF`, as [Linux epoll_ctl(2)](https://man7.org/linux/man-pages/man2/epoll_ctl.2.html) does; this error ordering is unmeasured on a console. Duplicate-descriptor alias lifetime is not modeled by this epoll backend. - [sceNetEpollControl and sceNetEpollWait](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) take sockets made with libkernel's `socket()`. PPSA02429 registers such sockets and retries the failed `ADD` and `DEL` about 1.6 million times in its first seconds. This follows the position taken for PPSA30839 in `sceNetSocketClose` and `sceNetSetsockopt`, that both libraries work on the same kernel sockets; epoll on them is not measured on a console. libkernel's `close()` and `sceKernelClose` retire their registrations at once, so a socket that reuses the number starts unregistered. - [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - console ordering between competing readers and writers is unmeasured. The per-call behavior follows FreeBSD 11.4's `soreceive_generic` and `sosend_generic` (`sys/kern/uipc_socket.c`); `0x80` is FreeBSD's flag value (`sys/sys/socket.h`). On Windows native sockets stay non-blocking. `MSG_DONTWAIT` and guest `SO_NBIO` return one host transfer's result. Blocking stream sends queue the whole buffer with one deadline and return progress after a timeout or host error. SSL uses this path through the native owner, retaining the socket, its mode and timeouts. Readiness waits poll at most every 10 ms for local close, so idle blocked operations wake periodically. -- [MSG_NOSIGNAL](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - `sceNetSend`, `sceNetSendto` and `sceNetSendmsg` accept `0x20000`, FreeBSD's `MSG_NOSIGNAL` (`sys/sys/socket.h`), which ProsperoTV (PPSA99003) passes on every phone-remote reply. Firmware 13.42's `libSceNet.sprx` adds `0x40020000` to the flags of every send and refuses no flag itself ([measured in an emulator](https://github.com/boykopovar/AnyPS5/pull/11037#issuecomment-6400759240)), so passing the flag changes nothing on the console. What `0x40000000` means and which flags the kernel refuses are unknown; other flags still fail with `EOPNOTSUPP` here. Host sends outside Windows already pass `MSG_NOSIGNAL` and Windows raises no `SIGPIPE`, so a send on a stream whose write side is shut down fails with `EPIPE`. Of 53 local titles, 35 call `sceNetSend` or `sceNetSendto` directly from their executables (2 tail jumps unread): flags 0 at 86 call sites, 0x80 at 10, non-constant at 2, never 0x40000000 or 0x20000. +- [MSG_NOSIGNAL](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - `sceNetSend`, `sceNetSendto` and `sceNetSendmsg` accept `0x20000`, FreeBSD's `MSG_NOSIGNAL` (`sys/sys/socket.h`), which ProsperoTV (PPSA99003) passes on every phone-remote reply. Firmware 13.42's `libSceNet.sprx` adds `0x40020000` to the flags of every send and refuses no flag itself ([measured in an emulator](https://github.com/boykopovar/AnyPS5/pull/11037#issuecomment-6400759240)), so passing the flag changes nothing on the console. What `0x40000000` means and which flags the kernel refuses are unknown; other flags still fail with `EOPNOTSUPP` here. Host sends outside Windows already pass `MSG_NOSIGNAL` and Windows raises no `SIGPIPE`, so a send on a stream whose write side is shut down fails with `EPIPE`. 35 of 53 local titles call `sceNetSend`/`sceNetSendto` directly from the executable (title libraries, function pointers, 2 tail jumps not read) with flags 0 at 86 sites, 0x80 at 10, non-constant at 2. - [Stream sends on macOS](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - a `MSG_DONTWAIT` transfer also passes XNU's `MSG_NBIO` (`0x20000`, absent from the SDK headers), because XNU's `MSG_DONTWAIT` does not keep a stream send from waiting for buffer space. A blocking stream send goes to the host in sends of 64 KiB, so an error after some progress, such as an abort, reports the bytes already queued, which one host send would drop; the bytes that the failing 64 KiB send had queued are not counted - [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) on a datagram socket with no local address wait on Windows and macOS until a bind, connect or send gives it one, checking the host socket every 10 ms. Winsock fails such a receive with `WSAEINVAL`, where FreeBSD 11.4's `soreceive_dgram` waits for a datagram (`sys/kern/uipc_socket.c`). On macOS the host receive waits, but a receive-side `shutdown` does not wake it. A non-blocking socket fails with `EAGAIN` at once, and the receive timeout ends the wait with `EAGAIN`; when the address comes first, the call waits for a datagram only for what is left of the timeout. A `sceNetShutdown` of the receive side ends the wait and the receive returns 0, and a zero-length receive returns 0 without waiting, as `soreceive_dgram` does when the socket can't receive more or nothing was asked for. An abort or close ends the wait, and the host receive then fails as it did before - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does From 655607481d17b366135c2dd7a919cc95409c13ca Mon Sep 17 00:00:00 2001 From: XairRrR Date: Thu, 29 Oct 2026 06:23:55 +0100 Subject: [PATCH 154/160] docs(build): list the draw committer and finisher among the trace buffer threads The performance trace section said that APS5_TRACE_EVENTS sizes the buffers of the queue workers and the presenter, and that only those threads record a window. The draw committer of pipelined draws, which is on by default, and the draw finisher of APS5_DRAW_PIPELINE also bind to the trace with the default capacity, so PerformanceTrace::BindThread gives them a buffer of APS5_TRACE_EVENTS events too, and they record their scopes in the window. The section now names both threads. AI-assisted: yes --- docs/dev/BUILD.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index b6034dec89..2d34ab0258 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -276,9 +276,9 @@ Set `APS5_SAMPLE_WORKER=` to sample queue worker 0 every millisecond (in p Set `APS5_TRACE_FRAMES=:` to record `` presents from present `` (counted from 0), or `APS5_TRACE_SECONDS=:` to record from the first present at least `` seconds after the first present until the first present `` seconds later. Both values are whole numbers and the length is above 0; a malformed value, or both switches together, fails the first present. The window opens and closes at presents. -Set `APS5_TRACE_EVENTS=` to change the queue-worker and presenter buffers for a frame or seconds window. The count is a whole number from 1 through 4,194,304; the default is 262,144. An invalid value fails when one of these threads first binds to the trace, before recording. Each buffer uses `count * sizeof(TraceThread::Slice)` bytes (currently 40 bytes per event, since each slice also carries its recorder batch): 10 MiB at the default, 120 MiB at `3145728`, and 160 MiB at the maximum, per thread. The buffers are allocated even while waiting for the window to open, and formatting the trace uses additional memory. Shader preparation keeps its explicitly sized 32,768-event buffers, as does a title thread named `title` that binds one the first time it registers a shader or resolves a shader ABI. The GPU range limit stays 262,144. Summary-only sessions allocate no event buffers and do not read this setting. A larger buffer can still fill; use a shorter window or increase the count within the bound. +Set `APS5_TRACE_EVENTS=` to change the buffers of the queue workers, the presenter, the draw committer of pipelined draws (`APS5_PIPELINED_DRAWS`) and the draw finisher (`APS5_DRAW_PIPELINE`) for a frame or seconds window. The count is a whole number from 1 through 4,194,304; the default is 262,144. An invalid value fails when one of these threads first binds to the trace, before recording. Each buffer uses `count * sizeof(TraceThread::Slice)` bytes (currently 40 bytes per event, since each slice also carries its recorder batch): 10 MiB at the default, 120 MiB at `3145728`, and 160 MiB at the maximum, per thread. The buffers are allocated even while waiting for the window to open, and formatting the trace uses additional memory. Shader preparation keeps its explicitly sized 32,768-event buffers, as does a title thread named `title` that binds one the first time it registers a shader or resolves a shader ABI. The GPU range limit stays 262,144. Summary-only sessions allocate no event buffers and do not read this setting. A larger buffer can still fill; use a shorter window or increase the count within the bound. -Inside the window the queue workers and the presenter record every `PerformanceTimer` scope and stage as a complete event named after its frame timing key: `Driver.WorkerWait`, `Driver.Packet`, `Driver.Packet.draw`, `Graphics.Draw`, `Driver.Present`, `Driver.Present.inflight_wait`, `Vulkan.Retire` and so on. A slice shorter than 20 us is only counted: its time is in the thread's counter ` folded` (milliseconds per present) and in the summary. A thread writes a present's counter sample at its first slice after the next present, so the last present of the window has no sample; its time is still in the summary. By default, each queue worker and presenter allocates a buffer for 262,144 events when it starts; a full buffer stops that thread's recording, sets `trace_dropped` and puts an instant event `trace buffer full` on the thread's track at the start of the first lost slice (slices are appended when their outer scope ends, so that can precede the last recorded slice by up to one outer-scope length), with the number of lost slices in `args.events_lost`. When the window closes, the presenter formats the trace once and a `ProfileOutput` writer thread writes `aps5-trace--.json.gz` (Chrome trace-event JSON, opened by ui.perfetto.dev or chrome://tracing) and appends one line to `aps5-summary.jsonl`, both in `APS5_TRACE_DIR` or the working directory. A window that is still open when the title exits writes nothing. +Inside the window the queue workers, the presenter and the draw committer and finisher record every `PerformanceTimer` scope and stage as a complete event named after its frame timing key: `Driver.WorkerWait`, `Driver.Packet`, `Driver.Packet.draw`, `Graphics.Draw`, `Driver.Present`, `Driver.Present.inflight_wait`, `Vulkan.Retire` and so on. A slice shorter than 20 us is only counted: its time is in the thread's counter ` folded` (milliseconds per present) and in the summary. A thread writes a present's counter sample at its first slice after the next present, so the last present of the window has no sample; its time is still in the summary. By default, each of these threads allocates a buffer for 262,144 events when it starts; a full buffer stops that thread's recording, sets `trace_dropped` and puts an instant event `trace buffer full` on the thread's track at the start of the first lost slice (slices are appended when their outer scope ends, so that can precede the last recorded slice by up to one outer-scope length), with the number of lost slices in `args.events_lost`. When the window closes, the presenter formats the trace once and a `ProfileOutput` writer thread writes `aps5-trace--.json.gz` (Chrome trace-event JSON, opened by ui.perfetto.dev or chrome://tracing) and appends one line to `aps5-summary.jsonl`, both in `APS5_TRACE_DIR` or the working directory. A window that is still open when the title exits writes nothing. The summary line has `commit` and `dirty` of the build, `env` (every `APS5_*` variable; a value containing `/` or `\` is written as ``), `gpu`, `driver`, `os`, `cpu` (the CPU brand string), `cpu_threads` and `memory_mib` (physical memory) of the host (all three absent on a host that is not x86 or reports no brand string), `title_id`, `title` and `content_version` from `app0/sce_sys/param.json` (only when the title has one; `content_version` is empty when the file has no `contentVersion`, or one that is not a string of at most 15 characters), `resolution` (the video output's), `window`, `presents`, `frame_ms` (p50, p90, p99 and max of the intervals between presents), `gpu_busy_ms_per_present` (the batch timestamps behind `[present]`), `worker_busy_pct` (share of the window each queue worker spends in `Driver.Packet`, `Driver.Suspend`, `Driver.EndSubmission`, `Driver.Rewind` and `Driver.Completion`), `top_phases_ms_per_frame` (the eight largest scopes and stages, without `Driver.WorkerWait`), `trace_dropped`, `trace_dropped_threads` and `trace`. `trace_dropped` is `true` when any thread lost a slice, any thread's totals table (512 scope and stage entries) was full, or the `gpu` track was full. `trace_dropped_threads` is present only then and has one entry per such thread name (threads that share a name, such as the shader prepare threads, share one entry: `events_lost` is summed, `events_full_after_s` is the earliest, `totals_full` is true if any is): `events_full_after_s` (seconds from the first present of the window to the start of the first lost slice; absent for `gpu` and, in every summary line (windowed `APS5_TRACE_FRAMES`/`APS5_TRACE_SECONDS` and `APS5_TRACE_SUMMARY_EVERY`), whenever the first lost slice starts outside the line's span of presents, including a slice whose scope began before the window opened or one lost after the window's last present), `events_lost` (slices not recorded; absent when 0; in `APS5_TRACE_SUMMARY_EVERY` lines it counts the thread buffer's losses since it started, not only those of the line's span) and `totals_full` (`true` when the thread's totals table overflowed, which makes the summary numbers incomplete; a thread with `events_lost` only has complete totals). A trace holds timings, scope names, counts and thread names; no guest memory, paths or strings of the title. From 04e35c008b63e46c5bee4d9ea1a533dbdaefeabe Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Thu, 29 Oct 2026 06:02:43 +0000 Subject: [PATCH 155/160] feat(libSceBeisobmf): implement 10 box metadata accessors --- core/libs/CMakeLists.txt | 10 ++ core/libs/prx/libSceBeisobmf/Export.cpp | 161 +++++++++++++------ core/libs/prx/libSceBeisobmf/include/Api.hpp | 25 +-- core/libs/tests/GuestBeisobmfBoxes.cpp | 112 +++++++++++++ 4 files changed, 248 insertions(+), 60 deletions(-) create mode 100644 core/libs/tests/GuestBeisobmfBoxes.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 0679e4c24b..fe8f6265a2 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -261,6 +261,16 @@ add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) include(prx/libSceLibreSsl/ProtocolSelection.cmake) if(BUILD_TESTING) + add_test_executable(guest_beisobmf_boxes_tests tests/GuestBeisobmfBoxes.cpp) + target_include_directories(guest_beisobmf_boxes_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_beisobmf_boxes_tests PRIVATE libSceBeisobmf libc) + configure_windows_unwind(guest_beisobmf_boxes_tests) + add_test(NAME guest_beisobmf_boxes COMMAND guest_beisobmf_boxes_tests) + if(WIN32) + set_tests_properties(guest_beisobmf_boxes PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_posix_scalar_io_tests tests/GuestPosixScalarIo.cpp) target_include_directories(guest_posix_scalar_io_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_posix_scalar_io_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libSceBeisobmf/Export.cpp b/core/libs/prx/libSceBeisobmf/Export.cpp index 31c0e4b2fe..2001e95233 100644 --- a/core/libs/prx/libSceBeisobmf/Export.cpp +++ b/core/libs/prx/libSceBeisobmf/Export.cpp @@ -1,100 +1,163 @@ +#include "SceTypes.hpp" #include "prx/libSceBeisobmf/include/Api.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/GuestArena.hpp" #include #include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" +#include +#include + +namespace { + +template +TValue ReadField(const void *object, std::size_t offset) { + if (!object) + throw std::invalid_argument("isobmf box pointer is null"); + TValue value{}; + std::memcpy(&value, static_cast(object) + offset, + sizeof(value)); + return value; +} + +std::uintptr_t ReadPointer(const void *object, std::size_t offset) { + return ReadField(object, offset); +} + +void WritePointer(void *object, std::size_t offset, std::uintptr_t value) { + std::memcpy(static_cast(object) + offset, &value, sizeof(value)); +} + +std::uintptr_t FindMember(void *box, const char *name) { + if (!box || !name) + throw std::invalid_argument("isobmf member lookup requires a box and name"); + auto *cursor = static_cast(box) + 0x50; + const GuestArena::HostWrite write(cursor, sizeof(std::uintptr_t)); + if (!write.Open()) + throw std::runtime_error( + "isobmf member cursor is not writable guest memory"); + WritePointer(box, 0x50, ReadPointer(box, 0x40)); + for (auto member = ReadPointer(box, 0x50); member != 0; + member = ReadPointer(box, 0x50)) { + const auto *memberName = reinterpret_cast(member + 0x18); + if (std::strncmp(memberName, name, 0x40) == 0) + return member; + WritePointer(box, 0x50, + ReadPointer(reinterpret_cast(member), 0x10)); + } + return 0; +} + +void *MemberPointer(std::uintptr_t member) { + const auto type = + ReadField(reinterpret_cast(member), 0x6c); + if (type < 20 && ((0x97800u >> (type & 0x1f)) & 1u) != 0) + return reinterpret_cast( + ReadPointer(reinterpret_cast(member), 0x60)); + return reinterpret_cast(member + 0x58); +} + +} // namespace extern "C" { -APS5_DUMMY_FUN -int APS5_VABI isobmf_box_getchildbox_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void *APS5_VABI isobmf_box_getchildbox_nid_postfix(void *box) { + return reinterpret_cast(ReadPointer(box, 0x10)); } -int APS5_VABI isobmf_box_getdiskoffset_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI isobmf_box_getdiskoffset_nid_postfix(void *box) { + return ReadField(box, 0x80); } -int APS5_VABI isobmf_box_getflags32b_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); +std::uint32_t APS5_VABI isobmf_box_getflags32b_nid_postfix(void *box) { + if (!box) return 0; + const auto member = FindMember(box, "flags"); + if (!member) + return 0; + const auto *value = static_cast(MemberPointer(member)); + return static_cast(value[0]) | + (static_cast(value[1]) << 8) | + (static_cast(value[2]) << 16); } -int APS5_VABI isobmf_box_getmember_length_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint32_t APS5_VABI +isobmf_box_getmember_length_nid_postfix(void *box, const char *name) { + const auto member = FindMember(box, name); + return member ? ReadField( + reinterpret_cast(member), 0x68) + : 0; } -int APS5_VABI isobmf_box_getmember_ptr_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void *APS5_VABI isobmf_box_getmember_ptr_nid_postfix(void *box, + const char *name) { + const auto member = FindMember(box, name); + return member ? MemberPointer(member) : nullptr; } -int APS5_VABI isobmf_box_getmember_type_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint32_t APS5_VABI +isobmf_box_getmember_type_nid_postfix(void *box, const char *name) { + const auto member = FindMember(box, name); + return member ? ReadField( + reinterpret_cast(member), 0x6c) + : 0; } -int APS5_VABI isobmf_box_getnextbox_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void *APS5_VABI isobmf_box_getnextbox_nid_postfix(void *box) { + return reinterpret_cast(ReadPointer(box, 0x20)); } -int APS5_VABI isobmf_box_getstate_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint32_t APS5_VABI isobmf_box_getstate_nid_postfix(void *box) { + return ReadField(box, 0x8c); } -int APS5_VABI isobmf_box_gettype_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint32_t APS5_VABI isobmf_box_gettype_nid_postfix(void *box) { + return ReadField(box, 0x90); } int APS5_VABI isobmf_decoder_clear_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI isobmf_decoder_create_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI isobmf_decoder_decode_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI isobmf_decoder_destroy_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI isobmf_decoder_get_decoder_tree_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI isobmf_decoder_getquerydata_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } -int APS5_VABI isobmf_decoder_next_disk_offset_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI +isobmf_decoder_next_disk_offset_nid_postfix(void *decoder) { + if (!decoder) + return 0xffffffff; + return ReadField(decoder, 0xc0); } int APS5_VABI isobmf_decoder_query_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI isobmf_memmanager_set_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } - } diff --git a/core/libs/prx/libSceBeisobmf/include/Api.hpp b/core/libs/prx/libSceBeisobmf/include/Api.hpp index 60bc60a75e..51e0cc1ae1 100644 --- a/core/libs/prx/libSceBeisobmf/include/Api.hpp +++ b/core/libs/prx/libSceBeisobmf/include/Api.hpp @@ -4,23 +4,26 @@ extern "C" { -int APS5_VABI isobmf_box_getchildbox_nid_postfix(void); -int APS5_VABI isobmf_box_getdiskoffset_nid_postfix(void); -int APS5_VABI isobmf_box_getflags32b_nid_postfix(void); -int APS5_VABI isobmf_box_getmember_length_nid_postfix(void); -int APS5_VABI isobmf_box_getmember_ptr_nid_postfix(void); -int APS5_VABI isobmf_box_getmember_type_nid_postfix(void); -int APS5_VABI isobmf_box_getnextbox_nid_postfix(void); -int APS5_VABI isobmf_box_getstate_nid_postfix(void); -int APS5_VABI isobmf_box_gettype_nid_postfix(void); +void *APS5_VABI isobmf_box_getchildbox_nid_postfix(void *box); +std::uint64_t APS5_VABI isobmf_box_getdiskoffset_nid_postfix(void *box); +std::uint32_t APS5_VABI isobmf_box_getflags32b_nid_postfix(void *box); +std::uint32_t APS5_VABI +isobmf_box_getmember_length_nid_postfix(void *box, const char *name); +void *APS5_VABI isobmf_box_getmember_ptr_nid_postfix(void *box, + const char *name); +std::uint32_t APS5_VABI isobmf_box_getmember_type_nid_postfix(void *box, + const char *name); +void *APS5_VABI isobmf_box_getnextbox_nid_postfix(void *box); +std::uint32_t APS5_VABI isobmf_box_getstate_nid_postfix(void *box); +std::uint32_t APS5_VABI isobmf_box_gettype_nid_postfix(void *box); int APS5_VABI isobmf_decoder_clear_nid_postfix(void); int APS5_VABI isobmf_decoder_create_nid_postfix(void); int APS5_VABI isobmf_decoder_decode_nid_postfix(void); int APS5_VABI isobmf_decoder_destroy_nid_postfix(void); int APS5_VABI isobmf_decoder_get_decoder_tree_nid_postfix(void); int APS5_VABI isobmf_decoder_getquerydata_nid_postfix(void); -int APS5_VABI isobmf_decoder_next_disk_offset_nid_postfix(void); +std::uint64_t APS5_VABI +isobmf_decoder_next_disk_offset_nid_postfix(void *decoder); int APS5_VABI isobmf_decoder_query_nid_postfix(void); int APS5_VABI isobmf_memmanager_set_nid_postfix(void); - } diff --git a/core/libs/tests/GuestBeisobmfBoxes.cpp b/core/libs/tests/GuestBeisobmfBoxes.cpp new file mode 100644 index 0000000000..224fdd9c08 --- /dev/null +++ b/core/libs/tests/GuestBeisobmfBoxes.cpp @@ -0,0 +1,112 @@ +#include "prx/libSceBeisobmf/include/Api.hpp" +#include "prx/libc/include/GuestArena.hpp" + +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const char *message) { + if (!condition) { + std::fprintf(stderr, "BEISOBMF BOXES: %s\n", message); + std::abort(); + } +} + +void Write(void *destination, const void *source, std::size_t bytes) { + const GuestArena::HostWrite access(destination, bytes); + Require(access.Open(), "guest fixture memory was not writable"); + std::memcpy(destination, source, bytes); +} + +template +void WriteField(void *object, std::size_t offset, TValue value) { + Write(static_cast(object) + offset, &value, sizeof(value)); +} + +void *Allocate(std::size_t bytes) { + void *memory = GuestArena::GuestArenaAllocate_nid_postfix(bytes, 16); + Require(memory != nullptr, "guest fixture allocation failed"); + const std::array zeros{}; + auto *destination = static_cast(memory); + for (std::size_t offset = 0; offset < bytes; offset += zeros.size()) + Write(destination + offset, zeros.data(), + (bytes - offset < zeros.size()) ? bytes - offset : zeros.size()); + return memory; +} + +} // namespace + +int main() { + auto *box = static_cast(Allocate(0xa0)); + auto *inlineMember = static_cast(Allocate(0x70)); + auto *pointerMember = static_cast(Allocate(0x70)); + auto *payload = static_cast(Allocate(8)); + auto *child = Allocate(0x10); + auto *next = Allocate(0x10); + auto *decoder = static_cast(Allocate(0xd0)); + + WriteField(box, 0x10, child); + WriteField(box, 0x20, next); + WriteField(box, 0x40, inlineMember); + WriteField(box, 0x80, std::uint64_t{0x1122334455667788}); + WriteField(box, 0x8c, std::uint32_t{7}); + WriteField(box, 0x90, std::uint32_t{0x1234}); + WriteField(inlineMember, 0x10, pointerMember); + constexpr char flagsName[] = "flags"; + constexpr char inlineName[] = "inline"; + constexpr char pointerName[] = "external"; + Write(inlineMember + 0x18, flagsName, sizeof(flagsName)); + Write(inlineMember + 0x58, "\x34\x12\x01", 3); + WriteField(inlineMember, 0x68, std::uint32_t{3}); + WriteField(inlineMember, 0x6c, std::uint32_t{0}); + Write(pointerMember + 0x18, pointerName, sizeof(pointerName)); + WriteField(pointerMember, 0x68, std::uint32_t{8}); + WriteField(pointerMember, 0x6c, std::uint32_t{11}); + WriteField(pointerMember, 0x60, payload); + WriteField(decoder, 0xc0, std::uint64_t{0xfedcba9876543210}); + + Require(isobmf_box_getchildbox_nid_postfix(box) == child, + "child box offset is wrong"); + Require(isobmf_box_getnextbox_nid_postfix(box) == next, + "next box offset is wrong"); + Require(isobmf_box_getdiskoffset_nid_postfix(box) == 0x1122334455667788, + "box disk offset is wrong"); + Require(isobmf_box_getstate_nid_postfix(box) == 7, + "box state offset is wrong"); + Require(isobmf_box_gettype_nid_postfix(box) == 0x1234, + "box type offset is wrong"); + Require(isobmf_box_getflags32b_nid_postfix(box) == 0x011234, + "flags value is wrong"); + Require(isobmf_box_getmember_length_nid_postfix(box, flagsName) == 3, + "inline member length is wrong"); + Require(isobmf_box_getmember_type_nid_postfix(box, flagsName) == 0, + "inline member type is wrong"); + Require(isobmf_box_getmember_ptr_nid_postfix(box, flagsName) == + inlineMember + 0x58, + "inline member pointer is wrong"); + Require(isobmf_box_getmember_length_nid_postfix(box, pointerName) == 8, + "external member length is wrong"); + Require(isobmf_box_getmember_type_nid_postfix(box, pointerName) == 11, + "external member type is wrong"); + Require(isobmf_box_getmember_ptr_nid_postfix(box, pointerName) == payload, + "external member pointer is wrong"); + Require(isobmf_box_getmember_ptr_nid_postfix(box, "missing") == nullptr, + "missing member pointer is not null"); + Require(isobmf_box_getmember_length_nid_postfix(box, "missing") == 0, + "missing member length is not zero"); + Require(isobmf_box_getmember_type_nid_postfix(box, "missing") == 0, + "missing member type is not zero"); + Require(isobmf_decoder_next_disk_offset_nid_postfix(decoder) == + 0xfedcba9876543210, + "decoder disk offset is wrong"); + Require(isobmf_decoder_next_disk_offset_nid_postfix(nullptr) == 0xffffffff, + "null decoder sentinel is wrong"); + + std::puts("BEISOBMF BOXES tests passed"); + return 0; +} From 6b3f09894799b61f50afe9bc53d9b30b1e9cef95 Mon Sep 17 00:00:00 2001 From: XairRrR Date: Thu, 29 Oct 2026 07:23:36 +0100 Subject: [PATCH 156/160] docs(progress): count a return of any local behaviourally empty wrapper as empty PROGRESS.md says a definition is behaviourally empty when it returns a local static wrapper that is itself behaviourally empty. The scanner recognizes wrappers with the same pattern it uses for stub wrappers, where static is optional, so a non-static helper such as int Support() { return 0; } also makes int APS5_VABI E() { return Support(); } behaviourally empty. Drop the static qualifier from the description. AI-assisted: yes --- docs/dev/PROGRESS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/PROGRESS.md b/docs/dev/PROGRESS.md index 3f40d5da94..47f52a5cd9 100644 --- a/docs/dev/PROGRESS.md +++ b/docs/dev/PROGRESS.md @@ -26,7 +26,7 @@ PR reports include a table of changed libraries with implemented export counts, New or removed libraries show `Not present` on the missing side. Libraries whose snapshots contain both counts and report zero exports show `No exports`; if either count is absent, the table shows `Counts unavailable`. Wrappers show `Shared sources` with the source library's name. These rows show `n/a` for the percentage change. The table retains the scanner's counting rules; it does not measure compatibility. -Implemented definitions are also classified as behaviourally empty when their body holds only `(void)name;` discards and at most one constant return, bare `return;` or `throw`, including a return of a local `static` wrapper that is itself behaviourally empty; wrapper calls whose arguments contain calls do not qualify. Comparisons list newly implemented behaviourally empty functions without changing any count. Older JSON snapshots without `inert_names` are accepted. +Implemented definitions are also classified as behaviourally empty when their body holds only `(void)name;` discards and at most one constant return, bare `return;` or `throw`, including a return of a local wrapper that is itself behaviourally empty; wrapper calls whose arguments contain calls do not qualify. Comparisons list newly implemented behaviourally empty functions without changing any count. Older JSON snapshots without `inert_names` are accepted. ## One library From 94095ef0c0bb97a1025bd45af551bca334ba9c81 Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Thu, 29 Oct 2026 06:36:12 +0000 Subject: [PATCH 157/160] fix(libSceBeisobmf): validate guest accessor memory --- core/libs/CMakeLists.txt | 11 +- core/libs/prx/libSceBeisobmf/Export.cpp | 216 ++++++++++++------- core/libs/prx/libSceBeisobmf/Tests.cmake | 11 + core/libs/prx/libSceBeisobmf/include/Api.hpp | 13 +- core/libs/tests/GuestBeisobmfBoxes.cpp | 193 ++++++++++------- docs/dev/debt/libSceBeisobmf.md | 7 +- 6 files changed, 279 insertions(+), 172 deletions(-) create mode 100644 core/libs/prx/libSceBeisobmf/Tests.cmake diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index fe8f6265a2..33325c9ef4 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -261,16 +261,6 @@ add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) include(prx/libSceLibreSsl/ProtocolSelection.cmake) if(BUILD_TESTING) - add_test_executable(guest_beisobmf_boxes_tests tests/GuestBeisobmfBoxes.cpp) - target_include_directories(guest_beisobmf_boxes_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_link_libraries(guest_beisobmf_boxes_tests PRIVATE libSceBeisobmf libc) - configure_windows_unwind(guest_beisobmf_boxes_tests) - add_test(NAME guest_beisobmf_boxes COMMAND guest_beisobmf_boxes_tests) - if(WIN32) - set_tests_properties(guest_beisobmf_boxes PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") - endif() - add_test_executable(guest_posix_scalar_io_tests tests/GuestPosixScalarIo.cpp) target_include_directories(guest_posix_scalar_io_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_posix_scalar_io_tests PRIVATE libkernel libc) @@ -1128,3 +1118,4 @@ if(BUILD_TESTING) endif() include(prx/libSceFreeType/CurveBounds.cmake) +include(prx/libSceBeisobmf/Tests.cmake) diff --git a/core/libs/prx/libSceBeisobmf/Export.cpp b/core/libs/prx/libSceBeisobmf/Export.cpp index 2001e95233..4014f85fed 100644 --- a/core/libs/prx/libSceBeisobmf/Export.cpp +++ b/core/libs/prx/libSceBeisobmf/Export.cpp @@ -1,59 +1,113 @@ #include "SceTypes.hpp" #include "prx/libSceBeisobmf/include/Api.hpp" #include "prx/libc/include/General.hpp" -#include "prx/libc/include/GuestArena.hpp" #include #include +#include #include +#include #include +#include namespace { +std::uintptr_t FieldAddress(const void *object, std::size_t offset) { + const auto address = reinterpret_cast(object); + if (address == 0) throw std::invalid_argument("isobmf object pointer is null"); + if (offset > std::numeric_limits::max() - address) + throw std::runtime_error("isobmf field address overflows"); + return address + offset; +} + +template +TValue ReadAt(std::uintptr_t address) { + TValue value{}; + if (address == 0 || + !GuestCopyIn_nid_no_patch(&value, reinterpret_cast(address), sizeof(value))) + throw std::runtime_error("isobmf guest field is unreadable"); + return value; +} + template TValue ReadField(const void *object, std::size_t offset) { - if (!object) - throw std::invalid_argument("isobmf box pointer is null"); - TValue value{}; - std::memcpy(&value, static_cast(object) + offset, - sizeof(value)); - return value; + return ReadAt(FieldAddress(object, offset)); } std::uintptr_t ReadPointer(const void *object, std::size_t offset) { - return ReadField(object, offset); + return ReadField(object, offset); } void WritePointer(void *object, std::size_t offset, std::uintptr_t value) { - std::memcpy(static_cast(object) + offset, &value, sizeof(value)); + const auto address = FieldAddress(object, offset); + if (!GuestCopyOut_nid_no_patch(reinterpret_cast(address), &value, sizeof(value))) + throw std::runtime_error("isobmf guest cursor is not writable"); +} + +std::array CopyRequestedName(const char *name) { + std::array result{}; + const auto address = reinterpret_cast(name); + if (address == 0) throw std::invalid_argument("isobmf member name is null"); + for (std::size_t i = 0; i < result.size(); ++i) { + if (i > std::numeric_limits::max() - address || + !GuestCopyIn_nid_no_patch(&result[i], reinterpret_cast(address + i), + sizeof(result[i]))) + throw std::runtime_error("isobmf member name is unreadable"); + if (result[i] == '\0') break; + } + return result; } std::uintptr_t FindMember(void *box, const char *name) { - if (!box || !name) - throw std::invalid_argument("isobmf member lookup requires a box and name"); - auto *cursor = static_cast(box) + 0x50; - const GuestArena::HostWrite write(cursor, sizeof(std::uintptr_t)); - if (!write.Open()) - throw std::runtime_error( - "isobmf member cursor is not writable guest memory"); - WritePointer(box, 0x50, ReadPointer(box, 0x40)); - for (auto member = ReadPointer(box, 0x50); member != 0; - member = ReadPointer(box, 0x50)) { - const auto *memberName = reinterpret_cast(member + 0x18); - if (std::strncmp(memberName, name, 0x40) == 0) - return member; - WritePointer(box, 0x50, - ReadPointer(reinterpret_cast(member), 0x10)); - } - return 0; + if (!box || !name) + throw std::invalid_argument("isobmf member lookup requires a box and name"); + const auto requestedName = CopyRequestedName(name); + const auto boxAddress = reinterpret_cast(box); + WritePointer(box, 0x50, ReadPointer(box, 0x40)); + std::unordered_set visited; + auto member = ReadPointer(box, 0x50); + while (member != 0) { + if (!visited.insert(member).second) + throw std::runtime_error("isobmf member list contains a cycle"); + std::array memberName{}; + const auto nameAddress = FieldAddress(reinterpret_cast(member), 0x18); + if (!GuestCopyIn_nid_no_patch(memberName.data(), + reinterpret_cast(nameAddress), memberName.size())) + throw std::runtime_error("isobmf member key is unreadable"); + if (std::strncmp(memberName.data(), requestedName.data(), memberName.size()) == 0) + return member; + member = ReadPointer(reinterpret_cast(member), 0x10); + WritePointer(reinterpret_cast(boxAddress), 0x50, member); + } + return 0; } void *MemberPointer(std::uintptr_t member) { - const auto type = - ReadField(reinterpret_cast(member), 0x6c); - if (type < 20 && ((0x97800u >> (type & 0x1f)) & 1u) != 0) - return reinterpret_cast( - ReadPointer(reinterpret_cast(member), 0x60)); - return reinterpret_cast(member + 0x58); + const auto type = + ReadField(reinterpret_cast(member), 0x6c); + if (type < 20 && ((0x97800u >> (type & 0x1f)) & 1u) != 0) + return reinterpret_cast( + ReadPointer(reinterpret_cast(member), 0x60)); + return reinterpret_cast(FieldAddress(reinterpret_cast(member), 0x58)); +} + +void LogGuestAccessFailure(const char *function, const char *message) noexcept { + try { + APS5_LOG_ERR("%s failed safely: %s", function, message); + } catch (...) { + } +} + +template +TValue CallSafely(const char *function, TValue fallback, TFunction operation) noexcept { + try { + return operation(); + } catch (const std::exception &error) { + LogGuestAccessFailure(function, error.what()); + return fallback; + } catch (...) { + LogGuestAccessFailure(function, "unknown guest memory error"); + return fallback; + } } } // namespace @@ -61,103 +115,115 @@ void *MemberPointer(std::uintptr_t member) { extern "C" { void *APS5_VABI isobmf_box_getchildbox_nid_postfix(void *box) { - return reinterpret_cast(ReadPointer(box, 0x10)); + return CallSafely(__func__, static_cast(nullptr), [&] { + return reinterpret_cast(ReadPointer(box, 0x10)); + }); } std::uint64_t APS5_VABI isobmf_box_getdiskoffset_nid_postfix(void *box) { - return ReadField(box, 0x80); + return CallSafely(__func__, std::uint64_t{0}, [&] { return ReadField(box, 0x80); }); } std::uint32_t APS5_VABI isobmf_box_getflags32b_nid_postfix(void *box) { - if (!box) - return 0; - const auto member = FindMember(box, "flags"); - if (!member) - return 0; - const auto *value = static_cast(MemberPointer(member)); - return static_cast(value[0]) | - (static_cast(value[1]) << 8) | - (static_cast(value[2]) << 16); + return CallSafely(__func__, std::uint32_t{0}, [&] { + if (!box) return std::uint32_t{0}; + const auto member = FindMember(box, "flags"); + if (!member) return std::uint32_t{0}; + std::array value{}; + const auto address = reinterpret_cast(MemberPointer(member)); + if (!GuestCopyIn_nid_no_patch(value.data(), reinterpret_cast(address), + value.size())) + throw std::runtime_error("isobmf flags payload is unreadable"); + return static_cast(value[0]) | + (static_cast(value[1]) << 8) | + (static_cast(value[2]) << 16); + }); } std::uint32_t APS5_VABI isobmf_box_getmember_length_nid_postfix(void *box, const char *name) { - const auto member = FindMember(box, name); - return member ? ReadField( - reinterpret_cast(member), 0x68) - : 0; + return CallSafely(__func__, std::uint32_t{0}, [&] { + const auto member = FindMember(box, name); + return member ? ReadField(reinterpret_cast(member), 0x68) : 0; + }); } void *APS5_VABI isobmf_box_getmember_ptr_nid_postfix(void *box, const char *name) { - const auto member = FindMember(box, name); - return member ? MemberPointer(member) : nullptr; + return CallSafely(__func__, static_cast(nullptr), [&] { + const auto member = FindMember(box, name); + return member ? MemberPointer(member) : nullptr; + }); } std::uint32_t APS5_VABI isobmf_box_getmember_type_nid_postfix(void *box, const char *name) { - const auto member = FindMember(box, name); - return member ? ReadField( - reinterpret_cast(member), 0x6c) - : 0; + return CallSafely(__func__, std::uint32_t{0}, [&] { + const auto member = FindMember(box, name); + return member ? ReadField(reinterpret_cast(member), 0x6c) : 0; + }); } void *APS5_VABI isobmf_box_getnextbox_nid_postfix(void *box) { - return reinterpret_cast(ReadPointer(box, 0x20)); + return CallSafely(__func__, static_cast(nullptr), [&] { + return reinterpret_cast(ReadPointer(box, 0x20)); + }); } std::uint32_t APS5_VABI isobmf_box_getstate_nid_postfix(void *box) { - return ReadField(box, 0x8c); + return CallSafely(__func__, std::uint32_t{0}, [&] { return ReadField(box, 0x8c); }); } std::uint32_t APS5_VABI isobmf_box_gettype_nid_postfix(void *box) { - return ReadField(box, 0x90); + return CallSafely(__func__, std::uint32_t{0}, [&] { return ReadField(box, 0x90); }); } int APS5_VABI isobmf_decoder_clear_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI isobmf_decoder_create_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI isobmf_decoder_decode_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI isobmf_decoder_destroy_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI isobmf_decoder_get_decoder_tree_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI isobmf_decoder_getquerydata_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } std::uint64_t APS5_VABI isobmf_decoder_next_disk_offset_nid_postfix(void *decoder) { - if (!decoder) - return 0xffffffff; - return ReadField(decoder, 0xc0); + return CallSafely(__func__, std::uint64_t{0xffffffff}, [&] { + if (!decoder) return std::uint64_t{0xffffffff}; + return ReadField(decoder, 0xc0); + }); } int APS5_VABI isobmf_decoder_query_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI isobmf_memmanager_set_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + NotImplemented_nid_no_patch(__func__); + return 0; } + } diff --git a/core/libs/prx/libSceBeisobmf/Tests.cmake b/core/libs/prx/libSceBeisobmf/Tests.cmake new file mode 100644 index 0000000000..ae50a7d50e --- /dev/null +++ b/core/libs/prx/libSceBeisobmf/Tests.cmake @@ -0,0 +1,11 @@ +if(BUILD_TESTING) + add_test_executable(guest_beisobmf_boxes_tests ${CMAKE_CURRENT_LIST_DIR}/../../tests/GuestBeisobmfBoxes.cpp) + target_include_directories(guest_beisobmf_boxes_tests PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../..) + target_link_libraries(guest_beisobmf_boxes_tests PRIVATE libSceBeisobmf libc) + configure_windows_unwind(guest_beisobmf_boxes_tests) + add_test(NAME guest_beisobmf_boxes COMMAND guest_beisobmf_boxes_tests) + if(WIN32) + set_tests_properties(guest_beisobmf_boxes PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceBeisobmf/include/Api.hpp b/core/libs/prx/libSceBeisobmf/include/Api.hpp index 51e0cc1ae1..0cce4a491c 100644 --- a/core/libs/prx/libSceBeisobmf/include/Api.hpp +++ b/core/libs/prx/libSceBeisobmf/include/Api.hpp @@ -7,12 +7,9 @@ extern "C" { void *APS5_VABI isobmf_box_getchildbox_nid_postfix(void *box); std::uint64_t APS5_VABI isobmf_box_getdiskoffset_nid_postfix(void *box); std::uint32_t APS5_VABI isobmf_box_getflags32b_nid_postfix(void *box); -std::uint32_t APS5_VABI -isobmf_box_getmember_length_nid_postfix(void *box, const char *name); -void *APS5_VABI isobmf_box_getmember_ptr_nid_postfix(void *box, - const char *name); -std::uint32_t APS5_VABI isobmf_box_getmember_type_nid_postfix(void *box, - const char *name); +std::uint32_t APS5_VABI isobmf_box_getmember_length_nid_postfix(void *box, const char *name); +void *APS5_VABI isobmf_box_getmember_ptr_nid_postfix(void *box, const char *name); +std::uint32_t APS5_VABI isobmf_box_getmember_type_nid_postfix(void *box, const char *name); void *APS5_VABI isobmf_box_getnextbox_nid_postfix(void *box); std::uint32_t APS5_VABI isobmf_box_getstate_nid_postfix(void *box); std::uint32_t APS5_VABI isobmf_box_gettype_nid_postfix(void *box); @@ -22,8 +19,8 @@ int APS5_VABI isobmf_decoder_decode_nid_postfix(void); int APS5_VABI isobmf_decoder_destroy_nid_postfix(void); int APS5_VABI isobmf_decoder_get_decoder_tree_nid_postfix(void); int APS5_VABI isobmf_decoder_getquerydata_nid_postfix(void); -std::uint64_t APS5_VABI -isobmf_decoder_next_disk_offset_nid_postfix(void *decoder); +std::uint64_t APS5_VABI isobmf_decoder_next_disk_offset_nid_postfix(void *decoder); int APS5_VABI isobmf_decoder_query_nid_postfix(void); int APS5_VABI isobmf_memmanager_set_nid_postfix(void); + } diff --git a/core/libs/tests/GuestBeisobmfBoxes.cpp b/core/libs/tests/GuestBeisobmfBoxes.cpp index 224fdd9c08..58c2ddbaa0 100644 --- a/core/libs/tests/GuestBeisobmfBoxes.cpp +++ b/core/libs/tests/GuestBeisobmfBoxes.cpp @@ -11,102 +11,139 @@ namespace { void Require(bool condition, const char *message) { - if (!condition) { + if (condition) return; std::fprintf(stderr, "BEISOBMF BOXES: %s\n", message); std::abort(); - } } void Write(void *destination, const void *source, std::size_t bytes) { - const GuestArena::HostWrite access(destination, bytes); - Require(access.Open(), "guest fixture memory was not writable"); - std::memcpy(destination, source, bytes); + const GuestArena::HostWrite access(destination, bytes); + Require(access.Open(), "guest fixture memory was not writable"); + std::memcpy(destination, source, bytes); } template void WriteField(void *object, std::size_t offset, TValue value) { - Write(static_cast(object) + offset, &value, sizeof(value)); + Write(static_cast(object) + offset, &value, sizeof(value)); } void *Allocate(std::size_t bytes) { - void *memory = GuestArena::GuestArenaAllocate_nid_postfix(bytes, 16); - Require(memory != nullptr, "guest fixture allocation failed"); - const std::array zeros{}; - auto *destination = static_cast(memory); - for (std::size_t offset = 0; offset < bytes; offset += zeros.size()) - Write(destination + offset, zeros.data(), - (bytes - offset < zeros.size()) ? bytes - offset : zeros.size()); - return memory; + void *memory = GuestArena::GuestArenaAllocate_nid_postfix(bytes, 16); + Require(memory != nullptr, "guest fixture allocation failed"); + const std::array zeros{}; + auto *destination = static_cast(memory); + for (std::size_t offset = 0; offset < bytes; offset += zeros.size()) { + const auto count = (bytes - offset < zeros.size()) ? bytes - offset : zeros.size(); + Write(destination + offset, zeros.data(), count); + } + return memory; +} + +std::uintptr_t ReadCursor(const void *box) { + std::uintptr_t cursor{}; + std::memcpy(&cursor, static_cast(box) + 0x50, sizeof(cursor)); + return cursor; } } // namespace int main() { - auto *box = static_cast(Allocate(0xa0)); - auto *inlineMember = static_cast(Allocate(0x70)); - auto *pointerMember = static_cast(Allocate(0x70)); - auto *payload = static_cast(Allocate(8)); - auto *child = Allocate(0x10); - auto *next = Allocate(0x10); - auto *decoder = static_cast(Allocate(0xd0)); + auto *box = static_cast(Allocate(0xa0)); + auto *inlineMember = static_cast(Allocate(0x70)); + auto *pointerMember = static_cast(Allocate(0x70)); + auto *payload = static_cast(Allocate(8)); + auto *child = Allocate(0x10); + auto *next = Allocate(0x10); + auto *decoder = static_cast(Allocate(0xd0)); - WriteField(box, 0x10, child); - WriteField(box, 0x20, next); - WriteField(box, 0x40, inlineMember); - WriteField(box, 0x80, std::uint64_t{0x1122334455667788}); - WriteField(box, 0x8c, std::uint32_t{7}); - WriteField(box, 0x90, std::uint32_t{0x1234}); - WriteField(inlineMember, 0x10, pointerMember); - constexpr char flagsName[] = "flags"; - constexpr char inlineName[] = "inline"; - constexpr char pointerName[] = "external"; - Write(inlineMember + 0x18, flagsName, sizeof(flagsName)); - Write(inlineMember + 0x58, "\x34\x12\x01", 3); - WriteField(inlineMember, 0x68, std::uint32_t{3}); - WriteField(inlineMember, 0x6c, std::uint32_t{0}); - Write(pointerMember + 0x18, pointerName, sizeof(pointerName)); - WriteField(pointerMember, 0x68, std::uint32_t{8}); - WriteField(pointerMember, 0x6c, std::uint32_t{11}); - WriteField(pointerMember, 0x60, payload); - WriteField(decoder, 0xc0, std::uint64_t{0xfedcba9876543210}); + WriteField(box, 0x10, child); + WriteField(box, 0x20, next); + WriteField(box, 0x40, inlineMember); + WriteField(box, 0x80, std::uint64_t{0x1122334455667788}); + WriteField(box, 0x8c, std::uint32_t{7}); + WriteField(box, 0x90, std::uint32_t{0x1234}); + WriteField(inlineMember, 0x10, pointerMember); + constexpr char flagsName[] = "flags"; + constexpr char pointerName[] = "external"; + Write(inlineMember + 0x18, flagsName, sizeof(flagsName)); + Write(inlineMember + 0x58, "\x34\x12\x01", 3); + WriteField(inlineMember, 0x68, std::uint32_t{3}); + WriteField(inlineMember, 0x6c, std::uint32_t{0}); + Write(pointerMember + 0x18, pointerName, sizeof(pointerName)); + WriteField(pointerMember, 0x68, std::uint32_t{8}); + WriteField(pointerMember, 0x6c, std::uint32_t{11}); + WriteField(pointerMember, 0x60, payload); + WriteField(decoder, 0xc0, std::uint64_t{0xfedcba9876543210}); - Require(isobmf_box_getchildbox_nid_postfix(box) == child, - "child box offset is wrong"); - Require(isobmf_box_getnextbox_nid_postfix(box) == next, - "next box offset is wrong"); - Require(isobmf_box_getdiskoffset_nid_postfix(box) == 0x1122334455667788, - "box disk offset is wrong"); - Require(isobmf_box_getstate_nid_postfix(box) == 7, - "box state offset is wrong"); - Require(isobmf_box_gettype_nid_postfix(box) == 0x1234, - "box type offset is wrong"); - Require(isobmf_box_getflags32b_nid_postfix(box) == 0x011234, - "flags value is wrong"); - Require(isobmf_box_getmember_length_nid_postfix(box, flagsName) == 3, - "inline member length is wrong"); - Require(isobmf_box_getmember_type_nid_postfix(box, flagsName) == 0, - "inline member type is wrong"); - Require(isobmf_box_getmember_ptr_nid_postfix(box, flagsName) == - inlineMember + 0x58, - "inline member pointer is wrong"); - Require(isobmf_box_getmember_length_nid_postfix(box, pointerName) == 8, - "external member length is wrong"); - Require(isobmf_box_getmember_type_nid_postfix(box, pointerName) == 11, - "external member type is wrong"); - Require(isobmf_box_getmember_ptr_nid_postfix(box, pointerName) == payload, - "external member pointer is wrong"); - Require(isobmf_box_getmember_ptr_nid_postfix(box, "missing") == nullptr, - "missing member pointer is not null"); - Require(isobmf_box_getmember_length_nid_postfix(box, "missing") == 0, - "missing member length is not zero"); - Require(isobmf_box_getmember_type_nid_postfix(box, "missing") == 0, - "missing member type is not zero"); - Require(isobmf_decoder_next_disk_offset_nid_postfix(decoder) == - 0xfedcba9876543210, - "decoder disk offset is wrong"); - Require(isobmf_decoder_next_disk_offset_nid_postfix(nullptr) == 0xffffffff, - "null decoder sentinel is wrong"); + Require(isobmf_box_getchildbox_nid_postfix(box) == child, "child box offset is wrong"); + Require(isobmf_box_getnextbox_nid_postfix(box) == next, "next box offset is wrong"); + Require(isobmf_box_getdiskoffset_nid_postfix(box) == 0x1122334455667788, + "box disk offset is wrong"); + Require(isobmf_box_getstate_nid_postfix(box) == 7, "box state offset is wrong"); + Require(isobmf_box_gettype_nid_postfix(box) == 0x1234, "box type offset is wrong"); + Require(isobmf_box_getflags32b_nid_postfix(box) == 0x011234, "flags value is wrong"); + Require(isobmf_box_getmember_length_nid_postfix(box, flagsName) == 3, + "inline member length is wrong"); + Require(isobmf_box_getmember_type_nid_postfix(box, flagsName) == 0, + "inline member type is wrong"); + Require(isobmf_box_getmember_ptr_nid_postfix(box, flagsName) == inlineMember + 0x58, + "inline member pointer is wrong"); + Require(isobmf_box_getmember_length_nid_postfix(box, pointerName) == 8, + "external member length is wrong"); + Require(isobmf_box_getmember_type_nid_postfix(box, pointerName) == 11, + "external member type is wrong"); + Require(isobmf_box_getmember_ptr_nid_postfix(box, pointerName) == payload, + "external member pointer is wrong"); + Require(ReadCursor(box) == reinterpret_cast(pointerMember), + "member lookup did not leave the cursor on the matched member"); - std::puts("BEISOBMF BOXES tests passed"); - return 0; + Require(isobmf_box_getmember_ptr_nid_postfix(box, "missing") == nullptr, + "missing member pointer is not null"); + Require(isobmf_box_getmember_length_nid_postfix(box, "missing") == 0, + "missing member length is not zero"); + Require(isobmf_box_getmember_type_nid_postfix(box, "missing") == 0, + "missing member type is not zero"); + Require(ReadCursor(box) == 0, "missing member lookup did not clear the cursor"); + Require(isobmf_decoder_next_disk_offset_nid_postfix(decoder) == 0xfedcba9876543210, + "decoder disk offset is wrong"); + + Require(isobmf_box_getflags32b_nid_postfix(nullptr) == 0, + "null flags box result is wrong"); + Require(isobmf_decoder_next_disk_offset_nid_postfix(nullptr) == 0xffffffff, + "null decoder sentinel is wrong"); + Require(isobmf_box_getchildbox_nid_postfix(nullptr) == nullptr, "null child box result is wrong"); + Require(isobmf_box_getdiskoffset_nid_postfix(nullptr) == 0, "null disk-offset result is wrong"); + Require(isobmf_box_getnextbox_nid_postfix(nullptr) == nullptr, "null next box result is wrong"); + Require(isobmf_box_getstate_nid_postfix(nullptr) == 0, "null state result is wrong"); + Require(isobmf_box_gettype_nid_postfix(nullptr) == 0, "null type result is wrong"); + Require(isobmf_box_getmember_length_nid_postfix(nullptr, flagsName) == 0, + "null member-length result is wrong"); + Require(isobmf_box_getmember_ptr_nid_postfix(nullptr, flagsName) == nullptr, + "null member-pointer result is wrong"); + Require(isobmf_box_getmember_type_nid_postfix(nullptr, flagsName) == 0, + "null member-type result is wrong"); + Require(isobmf_box_getmember_ptr_nid_postfix(box, nullptr) == nullptr, + "null member name result is wrong"); + + auto *badPointer = reinterpret_cast(std::uintptr_t{1}); + Require(isobmf_box_gettype_nid_postfix(badPointer) == 0, "unmapped box result is wrong"); + Require(isobmf_decoder_next_disk_offset_nid_postfix(badPointer) == 0xffffffff, + "unmapped decoder did not return its safe sentinel"); + auto *badName = reinterpret_cast(std::uintptr_t{1}); + Require(isobmf_box_getmember_ptr_nid_postfix(box, badName) == nullptr, + "unmapped member name result is wrong"); + auto *badListBox = static_cast(Allocate(0xa0)); + WriteField(badListBox, 0x40, std::uintptr_t{1}); + Require(isobmf_box_getmember_ptr_nid_postfix(badListBox, "missing") == nullptr, + "unmapped member list result is wrong"); + + auto *cycleBox = static_cast(Allocate(0xa0)); + auto *cycleMember = static_cast(Allocate(0x70)); + WriteField(cycleBox, 0x40, cycleMember); + WriteField(cycleMember, 0x10, cycleMember); + Require(isobmf_box_getmember_ptr_nid_postfix(cycleBox, "missing") == nullptr, + "cyclic member list did not fail safely"); + + std::puts("BEISOBMF BOXES tests passed"); + return 0; } diff --git a/docs/dev/debt/libSceBeisobmf.md b/docs/dev/debt/libSceBeisobmf.md index 44d43faa9f..7d5dc43a7a 100644 --- a/docs/dev/debt/libSceBeisobmf.md +++ b/docs/dev/debt/libSceBeisobmf.md @@ -2,4 +2,9 @@ ### Unknown function info -- [libSceBeisobmf pending exports](../../../core/libs/prx/libSceBeisobmf/Export.cpp) - 18 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceBeisobmf.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceBeisobmf pending exports](../../../core/libs/prx/libSceBeisobmf/Export.cpp) - 8 named callable exports remain among the 18 exports listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceBeisobmf.c) and cross-checked against PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [Ten box/decoder accessors](../../../core/libs/prx/libSceBeisobmf/Export.cpp) are implemented from Ghidra 12.1.4 static analysis of `libSceBeisobmf.sprx` in PS5 firmware 13.60. The layouts are not verified against a console. + - Box accessors: `getchildbox` at 0x3190 reads `box+0x10`; `getnextbox` at 0x31a0 reads `box+0x20`; `getdiskoffset` at 0x3220 reads `box+0x80`; `getstate` at 0x3200 reads `box+0x8c`; and `gettype` at 0x3210 reads `box+0x90`. + - Member lookup at 0x7ce0 uses the member list at `box+0x40` and cursor at `box+0x50`. Helpers 0x6ea0, 0x6ee0, and 0x6ec0 initialize, read, and advance the cursor through each member's `+0x10` link. Helper 0x7040 reads the member name at `+0x18`. `getmember_length` at 0x3130 uses helper 0x7060 to read `+0x68`; `getmember_type` at 0x3170 uses helper 0x7050 to read `+0x6c`; and `getmember_ptr` at 0x3150 uses helper 0x7000 to select inline data at `+0x58` or external data at `+0x60` when the type is `< 20` and its bit is set in mask `0x97800`. + - `getflags32b` at 0x31b0 looks up `"flags"`, uses the member pointer helper at 0x7000, then reads three bytes. `decoder_next_disk_offset` at 0x30b0 returns `0xffffffff` for a null decoder and otherwise reads `decoder+0xc0`. + - Safe host results for null or unreadable inputs are: null for pointer accessors, 0 for scalar accessors, and `0xffffffff` for `decoder_next_disk_offset`. The flags and decoder null results are visible in firmware; the remaining values are defensive fallbacks because exceptions must not escape these `APS5_VABI` exports. Guest fields are copied with `GuestCopyIn_nid_no_patch`; the member cursor is written with `GuestCopyOut_nid_no_patch`. Unreadable inputs and cyclic member lists fail inside the helper and become the safe fallback at the export boundary. From 522b8dc76f23444b1267d62ca3594229bb75b056 Mon Sep 17 00:00:00 2001 From: XairRrR Date: Thu, 29 Oct 2026 07:37:46 +0100 Subject: [PATCH 158/160] docs(architecture): list every case that builds a draw pipeline whole The pipeline library paragraph listed per-face depth bias, split alternate alpha blends, rect lists, geometry shaders and a stage without a variant id as the pipelines created whole. libraryKeys in Pipeline.cpp also builds a pipeline whole for barycentrics emulated through generated tessellation stages, for tessellation on MoltenVK, for a pixel shader with a fragment shader interlock or with its LDS array in a storage buffer, and for every pipeline under APS5_PIPELINE_STATS=1, and it tests a stage's RecompileResult::Identity rather than a variant id. The paragraph now lists these cases and names libraryKeys. AI-assisted: yes --- docs/dev/ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 57d0346c15..aee9d87899 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -138,7 +138,7 @@ flowchart LR - A draw or dispatch whose state matches no artifact prepared at registration (a title that builds its static ABI state itself, without `sceAgcDriverResolve*Abi`) gets one prepared at use by `SourceHandleFor` or `InvocationFor`, logged once per artifact and kept in the snapshot for the next use. - A stage that `sceAgcDriverResolve*Abi` cannot prepare for the state the title creates its pipeline with (a compute shader that calls user-data pointers, a descriptor the resource plan rejects) is left to its first use like a stage deferred at registration: `ResolveShaderAbi` and `PrepareGraphicsStages` catch the failure inside their own preparation transaction, so the `ShaderPreparationScope` the title's call runs in still commits, log it once per shader, mark the shader deferred and record the ABI key, so a repeated call prepares nothing; `ResolvePreparedGraphics` skips a deferred stage's rectangles until a draw has prepared it. A draw or dispatch that cannot prepare the stage either fails as before. Driver failures, `ProcessShutdown` and a failure stored by the shader's registration preparation still propagate from the ABI call. - A compute shader whose `s_swappc_b64` calls a user-data pointer cannot be linked at registration, because the callee's address is only known at dispatch. `InvocationFor` in [`ShaderRegistry.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) reads each callee from guest memory, appends one copy per call after the caller (`LinkUserDataCalls`) and prepares that linked code, so a different callee gets its own prepared entry. -- A draw's `Graphics::Pipeline` comes from `CachedPipeline`. When the device has `VK_EXT_graphics_pipeline_library` with fast linking, `VK_EXT_extended_dynamic_state` and `VK_KHR_dynamic_rendering`, draws render with `vkCmdBeginRenderingKHR` instead of render passes and the pipeline is linked from four libraries (vertex input, pre-rasterization shaders, fragment shader, fragment output) kept per device in [`PipelineLibrary.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp). A library built for one pipeline is reused by the next: another blend, vertex layout or target format builds only that part (the shader libraries name no attachment format), and cull mode, front face, depth and stencil state are dynamic, so changing them only links. The fast link is what the draw waits for; a background thread then links the same libraries with link-time optimization and the pipeline binds that one from the next draw on. Push constants then use a 256-byte block with a 128-byte slot per side: the pre-rasterization stages stack from byte 0, the pixel shader always starts at byte 128 (`StagePushOffset`), so its library does not depend on how much push data its vertex shader uses. Pipelines with per-face depth bias, a separate alpha blend into an 8_8 target with the alternate component order (both draw twice), rect lists, geometry shaders or a stage without a variant id are created whole, still with dynamic rendering; devices without the extensions (or with `APS5_NO_GPL=1`) keep render passes and whole pipelines. +- A draw's `Graphics::Pipeline` comes from `CachedPipeline`. When the device has `VK_EXT_graphics_pipeline_library` with fast linking, `VK_EXT_extended_dynamic_state` and `VK_KHR_dynamic_rendering`, draws render with `vkCmdBeginRenderingKHR` instead of render passes and the pipeline is linked from four libraries (vertex input, pre-rasterization shaders, fragment shader, fragment output) kept per device in [`PipelineLibrary.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp). A library built for one pipeline is reused by the next: another blend, vertex layout or target format builds only that part (the shader libraries name no attachment format), and cull mode, front face, depth and stencil state are dynamic, so changing them only links. The fast link is what the draw waits for; a background thread then links the same libraries with link-time optimization and the pipeline binds that one from the next draw on. Push constants then use a 256-byte block with a 128-byte slot per side: the pre-rasterization stages stack from byte 0, the pixel shader always starts at byte 128 (`StagePushOffset`), so its library does not depend on how much push data its vertex shader uses. Pipelines with per-face depth bias, a separate alpha blend into an 8_8 target with the alternate component order (both draw twice), rect lists, geometry shaders, barycentrics emulated through generated tessellation stages, tessellation on a driver without a usable tessellation pipeline cache (MoltenVK, `TessellationPipelineCacheUsable`), a pixel shader with a fragment shader interlock or with its LDS array in a storage buffer (`RecompileResult::functionLdsBuffer`), or a stage with an empty `RecompileResult::Identity`, and every pipeline under `APS5_PIPELINE_STATS=1`, are created whole (`libraryKeys` in [`Pipeline.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp)), still with dynamic rendering; devices without the extensions (or with `APS5_NO_GPL=1`) keep render passes and whole pipelines. - A storage or sampled view of a thin array whose whole surface has more layers than the device's `maxImageArrayLayers` (2048 on NVIDIA; a PS5 array may hold up to 8192) is served from its viewed slices alone: `ArrayLayerWindow` in [`TextureTiling.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) describes them as an array of their own starting at the first viewed slice, which is exact because a non-XOR swizzle does not depend on the slice index. A 3D volume is not an array (its depth is bounded by `maxImageDimension3D`) and a 2D image is one layer whatever its last array field holds; both pass unchanged. A view whose slices alone exceed the limit, a view of an XOR swizzle that mixes in the slice index, and any other image past the limit throw; a sampled lookup (`cachedTexture`) throws before it reads or snapshots the surface. - A draw whose `PA_SC_AA_CONFIG` selects 2, 4 or 8 samples renders into host-only multisampled images. [`MultisampleTarget.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) keeps one image per device, surface address, format, extent and sample count (`MultisampleTargetView`); the pipeline, render pass and library keys carry `State::samples`, and a multisampled depth target is a multisampled depth surface. The guest sample planes and FMASK are never read or written, so the registry also guards the target's guest range: a CPU read (`GuestMemory::SetReadGuard`), a DMA copy, and a sampled or storage image read of it throw. A `CB_COLOR_CONTROL` resolve mode over a multisampled CB0 runs as a render pass whose resolve attachment is CB1's resident image (`ResolveMultisampleTarget`). A zero fill of the target's CMASK (`NoteColorMetadataFill`, from fill kernels and CP DMA fills) clears the image at its next use. - A texture over a depth surface samples the depth image itself when it views one 2D slice of the plane's own format (`DepthSurfaceTexture` in [`DepthSurface.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp)). A view of several slices (a 2D array or a cube) or an integer view of a D16 plane is served by a `DepthPlaneCopy`, an image the slices are copied into at every use: from the depth surface bound at each slice's address, else from guest memory. Such a texture is never kept in the resource templates (`RefreshedPerUse`). From 4270b0d5fc2288b595000c532f6b08c5cdfe74ac Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:38:58 +0000 Subject: [PATCH 159/160] test(libSceBeisobmf): allocate committed guest fixtures --- core/libs/tests/GuestBeisobmfBoxes.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestBeisobmfBoxes.cpp b/core/libs/tests/GuestBeisobmfBoxes.cpp index 58c2ddbaa0..665b07248a 100644 --- a/core/libs/tests/GuestBeisobmfBoxes.cpp +++ b/core/libs/tests/GuestBeisobmfBoxes.cpp @@ -1,5 +1,6 @@ #include "prx/libSceBeisobmf/include/Api.hpp" #include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestHeap.hpp" #include #include @@ -28,7 +29,7 @@ void WriteField(void *object, std::size_t offset, TValue value) { } void *Allocate(std::size_t bytes) { - void *memory = GuestArena::GuestArenaAllocate_nid_postfix(bytes, 16); + void *memory = GuestHeap::GuestHeapAllocate_nid_postfix(bytes); Require(memory != nullptr, "guest fixture allocation failed"); const std::array zeros{}; auto *destination = static_cast(memory); From 11bdd371fa80f9cf3834c34372eccbbe1644ee2b Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Thu, 29 Oct 2026 21:35:14 +1300 Subject: [PATCH 160/160] fix(web): accept web release archives that predate newer web files The Pages deploy has failed since #11228 added failure-report.mjs to WEB_FILES: build_web.py --release required the release zip's file set to equal main's current WEB_FILES, but the latest release (v0.1.3) was packaged before that file existed, so every run stopped with "Invalid web release archive layout". Require the core files the site needs (index.html, app.mjs, worker.mjs and native/relinker_wasm.*) plus web-release.json, and allow only names from WEB_FILES. Unknown or duplicate entries, encrypted or empty files, the size limits and the tag/revision check are unchanged. The release is served as shipped, so it stays self-consistent. --- tools/build_web.py | 7 +++- tools/tests/test_build_web.py | 75 +++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/tools/build_web.py b/tools/build_web.py index c8f964bb76..3cd131ae04 100644 --- a/tools/build_web.py +++ b/tools/build_web.py @@ -14,6 +14,7 @@ import zipfile from package_release import WEB_FILES ROOT = Path(__file__).resolve().parent.parent +REQUIRED_WEB_FILES = ("index.html", "app.mjs", "worker.mjs", "native/relinker_wasm.mjs", "native/relinker_wasm.wasm") MAX_EXPANDED_BYTES = 512 * 1024 * 1024 @@ -169,8 +170,10 @@ def publish_release(output): staging.mkdir() with zipfile.ZipFile(io.BytesIO(data)) as archive: files = archive.infolist() - expected = {*WEB_FILES, "web-release.json"} - if len(files) != len(expected) or {file.filename for file in files} != expected: + names = [file.filename for file in files] + allowed = {*WEB_FILES, "web-release.json"} + if len(set(names)) != len(names) or not allowed.issuperset(names) \ + or not {*REQUIRED_WEB_FILES, "web-release.json"}.issubset(names): raise ValueError("Invalid web release archive layout") if any(file.flag_bits & 1 for file in files): raise ValueError(f"Encrypted release archive {asset['name']} is not supported") diff --git a/tools/tests/test_build_web.py b/tools/tests/test_build_web.py index 0b46ed03cb..d44dff4472 100644 --- a/tools/tests/test_build_web.py +++ b/tools/tests/test_build_web.py @@ -306,6 +306,81 @@ class ReleaseMetadataTests(unittest.TestCase): +class WebReleaseLayoutTests(unittest.TestCase): + TAG, REVISION = "v1", "a" * 40 + CORE = ("index.html", "app.mjs", "worker.mjs", "native/relinker_wasm.mjs", "native/relinker_wasm.wasm") + + def release(self, names, extra=(), metadata=None): + buffer = io.BytesIO() + with zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_DEFLATED) as archive: + for name in (*names, *extra): + archive.writestr(name, b"content " + name.encode()) + archive.writestr("web-release.json", json.dumps(metadata or {"tag": self.TAG, "revision": self.REVISION})) + return buffer.getvalue() + + def publish(self, data): + url = "https://example.invalid/web.zip" + release = {"tag_name": self.TAG, "html_url": "https://example.invalid/v1", + "assets": [{"name": f"web-{self.TAG}.zip", "size": len(data), "browser_download_url": url, + "digest": "sha256:" + hashlib.sha256(data).hexdigest()}]} + responses = {"https://api.github.com/repos/boykopovar/AnyPS5/releases/latest": json.dumps(release).encode(), url: data} + + def libraries(staging, _release): + (staging / "release.json").write_text(json.dumps({"tag": self.TAG}), encoding="utf-8") + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / "web" + with patch.object(build_web, "request", side_effect=lambda address: io.BytesIO(responses[address])), \ + patch.object(build_web, "library_assets"), patch.object(build_web, "publish_libraries", side_effect=libraries), \ + contextlib.redirect_stdout(io.StringIO()): + try: + build_web.publish_release(output) + finally: + self.published = sorted(str(file.relative_to(output)) for file in output.rglob("*") if file.is_file()) if output.exists() else [] + + def test_current_release_with_every_web_file_is_published(self): + self.publish(self.release(build_web.WEB_FILES)) + self.assertIn("failure-report.mjs", self.published) + + def test_release_lacking_a_newer_web_file_is_published_as_shipped(self): + older = [name for name in build_web.WEB_FILES if name != "failure-report.mjs"] + self.publish(self.release(older)) + self.assertNotIn("failure-report.mjs", self.published) + self.assertIn("app.mjs", self.published) + self.assertIn("release.json", self.published) + + def test_release_with_an_unknown_file_is_rejected(self): + for extra in ("unlisted.mjs", "native/extra.wasm", "../escape.mjs", "app.mjs.bak"): + with self.subTest(extra=extra): + with self.assertRaisesRegex(ValueError, "Invalid web release archive layout"): + self.publish(self.release(build_web.WEB_FILES, extra=(extra,))) + self.assertEqual(self.published, []) + + def test_release_missing_a_core_file_is_rejected(self): + for missing in self.CORE: + with self.subTest(missing=missing): + with self.assertRaisesRegex(ValueError, "Invalid web release archive layout"): + self.publish(self.release([name for name in build_web.WEB_FILES if name != missing])) + self.assertEqual(self.published, []) + + def test_release_with_a_duplicate_entry_is_rejected(self): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + data = self.release(build_web.WEB_FILES, extra=("app.mjs",)) + with self.assertRaisesRegex(ValueError, "Invalid web release archive layout"): + self.publish(data) + + def test_release_with_an_empty_file_or_mismatched_metadata_is_rejected(self): + with self.assertRaisesRegex(ValueError, "does not match the selected release"): + self.publish(self.release(build_web.WEB_FILES, metadata={"tag": "v0", "revision": self.REVISION})) + buffer = io.BytesIO() + with zipfile.ZipFile(buffer, "w") as archive: + for name in build_web.WEB_FILES: + archive.writestr(name, b"" if name == "app.mjs" else b"x") + archive.writestr("web-release.json", json.dumps({"tag": self.TAG, "revision": self.REVISION})) + with self.assertRaisesRegex(ValueError, "Invalid expanded web release archive size"): + self.publish(buffer.getvalue()) + + class LocalNativeArtifactTests(unittest.TestCase): def test_empty_or_missing_native_input_does_not_copy_sources(self): with tempfile.TemporaryDirectory() as directory: