From 7afd88926a07f72b0654eb4ea441cc493fcdb363 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 12 Oct 2026 18:34:22 +0000 Subject: [PATCH 01/81] fix(libkernel): accept MSG_DONTWAIT in the socket send and receive calls send, sendto, sendmsg, recv, recvfrom and recvmsg failed with EOPNOTSUPP for MSG_DONTWAIT (0x80), so code that polls a blocking socket with it got an error on every call instead of its data or EWOULDBLOCK. FreeBSD 11.4 defines the flag in sys/sys/socket.h: #define MSG_DONTWAIT 0x80 /* this message should be nonblocking */ and soreceive_generic() (sys/kern/uipc_socket.c) treats it as a non-blocking socket for that one call: if ((so->so_state & SS_NBIO) || (flags & (MSG_DONTWAIT|MSG_NBIO))) { SOCKBUF_UNLOCK(&so->so_rcv); error = EWOULDBLOCK; goto release; On Linux and macOS the flag now maps to the host's MSG_DONTWAIT, next to MSG_PEEK on a receive and MSG_NOSIGNAL on a send. Winsock has no per-call non-blocking flag, so Windows keeps failing with EOPNOTSUPP for it; TechnicalDebt records that. Other flags still fail with EOPNOTSUPP. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 34 +++++++++++++----- core/libs/tests/GuestUdp.cpp | 42 ++++++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 68 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 603a96cf356..648dcc5a081 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -47,13 +47,29 @@ using NativeSocket = int; constexpr auto Invalid = -1; #endif int Fail(int error) { *__error_nid_postfix() = error; return -1; } +constexpr int GuestPeek = 0x2; +constexpr int GuestDontWait = 0x80; constexpr int GuestNoSignal = 0x20000; +#ifdef _WIN32 +constexpr int GuestSendFlags = GuestNoSignal; +constexpr int GuestReceiveFlags = GuestPeek; +#else +constexpr int GuestSendFlags = GuestNoSignal | GuestDontWait; +constexpr int GuestReceiveFlags = GuestPeek | GuestDontWait; +#endif int NativeSendFlags(int flags) { #ifdef _WIN32 static_cast(flags); return 0; #else - return flags & GuestNoSignal ? MSG_NOSIGNAL : 0; + return (flags & GuestNoSignal ? MSG_NOSIGNAL : 0) | (flags & GuestDontWait ? MSG_DONTWAIT : 0); +#endif +} +int NativeReceiveFlags(int flags) { +#ifdef _WIN32 + return flags & GuestPeek ? MSG_PEEK : 0; +#else + return (flags & GuestPeek ? MSG_PEEK : 0) | (flags & GuestDontWait ? MSG_DONTWAIT : 0); #endif } int NativeError() { @@ -635,7 +651,7 @@ int APS5_VABI accept_nid_postfix(int descriptor, void* address, std::uint32_t* l std::int64_t APS5_VABI send_nid_postfix(int descriptor, const void* buffer, std::uint64_t length, int flags) { const auto socket = Lookup(descriptor); if (!socket) return -1; - if ((flags & ~GuestNoSignal) != 0) return Fail(45); + if ((flags & ~GuestSendFlags) != 0) return Fail(45); if (length > INT_MAX) return Fail(40); if (!buffer && length) return Fail(14); const auto result = ::send(socket->value, static_cast(buffer), static_cast(length), NativeSendFlags(flags)); @@ -644,13 +660,13 @@ std::int64_t APS5_VABI send_nid_postfix(int descriptor, const void* buffer, std: std::int64_t APS5_VABI recv_nid_postfix(int descriptor, void* buffer, std::uint64_t length, int flags) { const auto socket = Lookup(descriptor); if (!socket) return -1; - if ((flags & ~2) != 0) return Fail(45); + if ((flags & ~GuestReceiveFlags) != 0) return Fail(45); if (length > INT_MAX) return Fail(40); if (!buffer && length) return Fail(14); const GuestArena::HostWrite destination(length == 0 ? nullptr : buffer, static_cast(length)); if (!destination.Open()) return Fail(14); const auto result = ::recv(socket->value, static_cast(buffer), static_cast(length), - flags & 2 ? MSG_PEEK : 0); + NativeReceiveFlags(flags)); #ifdef _WIN32 if (result < 0 && socket->type == 2 && WSAGetLastError() == WSAEMSGSIZE) return static_cast(length); #endif @@ -720,7 +736,7 @@ std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, st int flags, const void* address, std::uint32_t addressLength) { const auto socket = Lookup(descriptor); if (!socket) return -1; - if ((flags & ~GuestNoSignal) != 0) return Fail(45); + if ((flags & ~GuestSendFlags) != 0) return Fail(45); if (length > INT_MAX) return Fail(40); if (!buffer && length) return Fail(14); int result; @@ -741,7 +757,7 @@ std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::u int flags, void* address, std::uint32_t* addressLength) { const auto socket = Lookup(descriptor); if (!socket) return -1; - if ((flags & ~2) != 0) return Fail(45); + if ((flags & ~GuestReceiveFlags) != 0) return Fail(45); if (length > INT_MAX) return Fail(40); if ((!buffer && length) || (address && !addressLength)) return Fail(14); const GuestArena::HostWrite destination(length == 0 ? nullptr : buffer, static_cast(length)); @@ -749,7 +765,7 @@ std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::u sockaddr_storage native{}; socklen_t size = sizeof(native); auto result = ::recvfrom(socket->value, static_cast(buffer), static_cast(length), - flags & 2 ? MSG_PEEK : 0, reinterpret_cast(&native), &size); + NativeReceiveFlags(flags), reinterpret_cast(&native), &size); #ifdef _WIN32 if (result < 0 && socket->type == 2 && WSAGetLastError() == WSAEMSGSIZE) result = static_cast(length); #endif @@ -813,7 +829,7 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, if (total < 0) return -1; const auto socket = Lookup(descriptor); if (!socket) return -1; - if ((flags & ~2) != 0) return Fail(45); + if ((flags & ~GuestReceiveFlags) != 0) return Fail(45); const bool records = socket->type == 2 || socket->type == 5; std::vector buffer; try { @@ -824,7 +840,7 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, sockaddr_storage native{}; socklen_t size = sizeof(native); std::int64_t received = ::recvfrom(socket->value, buffer.data(), static_cast(buffer.size()), - flags & 2 ? MSG_PEEK : 0, reinterpret_cast(&native), &size); + NativeReceiveFlags(flags), reinterpret_cast(&native), &size); if (received < 0) { #ifdef _WIN32 if (!records || WSAGetLastError() != WSAEMSGSIZE) return Fail(NativeError()); diff --git a/core/libs/tests/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp index 5b4dc269488..0fd3a8a6c8f 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -17,6 +17,7 @@ int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); int APS5_VABI ioctl_nid_postfix(int, std::uint64_t, void*); int APS5_VABI _ioctl_nid_postfix(int, std::uint64_t, void*); std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); +std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); std::int64_t APS5_VABI sendto_nid_postfix(int, const void*, std::uint64_t, int, const void*, std::uint32_t); std::int64_t APS5_VABI recvfrom_nid_postfix(int, void*, std::uint64_t, int, void*, std::uint32_t*); int APS5_VABI close_nid_postfix(int); @@ -96,9 +97,50 @@ static void CheckAddressLengths(int family) { Require(close_nid_postfix(sender) == 0 && close_nid_postfix(receiver) == 0); } +static void CheckDontWait() { + const int receiver = socket_nid_postfix(2, 2, 0); + const int sender = socket_nid_postfix(2, 2, 0); + Require(receiver >= 0 && sender >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(receiver, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(receiver, address.data(), &size) == 0); + char buffer[16]{}; + Iovec vector{buffer, sizeof(buffer)}; + Msghdr message{nullptr, 0, &vector, 1, nullptr, 0, 0}; +#ifdef _WIN32 + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == -1 && *__error_nid_postfix() == 45); + Require(recvfrom_nid_postfix(receiver, buffer, sizeof(buffer), 0x80, nullptr, nullptr) == -1 && *__error_nid_postfix() == 45); + Require(recvmsg_nid_postfix(receiver, &message, 0x80) == -1 && *__error_nid_postfix() == 45); + Require(sendto_nid_postfix(sender, "x", 1, 0x80, address.data(), address.size()) == -1 && *__error_nid_postfix() == 45); +#else + const auto start = std::chrono::steady_clock::now(); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == -1 && *__error_nid_postfix() == 35); + Require(recvfrom_nid_postfix(receiver, buffer, sizeof(buffer), 0x80, nullptr, nullptr) == -1 && *__error_nid_postfix() == 35); + Require(recvmsg_nid_postfix(receiver, &message, 0x80) == -1 && *__error_nid_postfix() == 35); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x82) == -1 && *__error_nid_postfix() == 35); + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(1)); + const char datagram[] = "nowait"; + Require(sendto_nid_postfix(sender, datagram, sizeof(datagram), 0x80, address.data(), address.size()) == sizeof(datagram)); + Require(recvfrom_nid_postfix(receiver, buffer, sizeof(buffer), 2, nullptr, nullptr) == sizeof(datagram)); + std::memset(buffer, 0, sizeof(buffer)); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x82) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); + std::memset(buffer, 0, sizeof(buffer)); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); + Require(connect_nid_postfix(sender, address.data(), address.size()) == 0); + Require(send_nid_postfix(sender, datagram, sizeof(datagram), 0x80 | 0x20000) == sizeof(datagram)); + std::memset(buffer, 0, sizeof(buffer)); + Require(recvmsg_nid_postfix(receiver, &message, 0) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == -1 && *__error_nid_postfix() == 35); +#endif + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x1) == -1 && *__error_nid_postfix() == 45); + Require(close_nid_postfix(receiver) == 0 && close_nid_postfix(sender) == 0); +} + int main() { CheckAddressLengths(2); CheckAddressLengths(28); + CheckDontWait(); const int receiver = socket_nid_postfix(2, 2, 0); const int sender = socket_nid_postfix(2, 2, 17); Require(receiver >= 0 && sender >= 0 && receiver != sender); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 132b6b9b421..9ea2ab7b65f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -131,6 +131,7 @@ - [execvp](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported. PPSA85300 imports it and fails to load without it; its `fork` and `vfork` fail with `ENOSYS` ([libc_missing.c](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/ps5/compat/libc_missing.c#L290-L292)), so the child process that would call it is never created. - [socketpair](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and a stream `send` after the peer closed reports `ECONNABORTED` instead of the console's `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLHUP` without `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, where the console reports `POLLIN`, with `POLLHUP` only after `close` - [UDP recv/recvfrom](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) and [sceNetRecv/sceNetRecvfrom](../../../core/libs/prx/libSceNet/Export.cpp) translate Windows WSAEMSGSIZE into the copied byte count for datagram sockets, including MSG_PEEK, and retain the sender address. This follows FreeBSD 11.4 recv(2) and Winsock's documented truncated-datagram behavior; console behavior and existing title impact are unverified. +- [MSG_DONTWAIT](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) in `send`, `sendto`, `sendmsg`, `recv`, `recvfrom` and `recvmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`). Winsock has no per-call non-blocking flag, so on Windows these calls still fail with `EOPNOTSUPP` for it - [send, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - send and sendto without an address on an unconnected datagram socket map Windows `WSAENOTCONN` to FreeBSD's `EDESTADDRREQ` (39). - [read, write and vector I/O](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. Guest socket descriptors start at `0x10000000` and are allocated upward, so the descriptor range selects the guest socket table or the host descriptor table. `EPIPE` from a write raises the host's `SIGPIPE` on Linux, which the guest can neither ignore nor suppress: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; `SO_NOSIGPIPE` is not an option here, and raises nothing on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor - [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits only on guest sockets, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A descriptor below the socket range (files, pipes, stdio) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), `POLLPRI` on Windows, which `WSAPoll` rejects, and an infinite timeout with no socket to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked From 2e465504be146762594b8605b4512e7576b02c3e Mon Sep 17 00:00:00 2001 From: QRMOE Date: Tue, 13 Oct 2026 01:02:19 +0200 Subject: [PATCH 02/81] fix(shader): honor disabled optimization during specialization --- core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp index a1b5e245b21..89702e7b2bd 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp @@ -94,7 +94,7 @@ std::vector ValidateAndOptimizeSpirv(std::span(spirv.begin(), spirv.end()); + if (mode != nullptr && std::string(mode) == "none") return std::vector(spirv.begin(), spirv.end()); // The recompiler emits helpers (BDA lookup, fault reporting) as functions called from every memory // access. Exhaustive inlining multiplies module size by ~10x and optimization time by ~20x, so the // default pipeline keeps the performance passes that work per function and leaves inlining to the driver. From cac1355283f9c20e1ef5f6c9cae52f55b58473e8 Mon Sep 17 00:00:00 2001 From: QRMOE Date: Tue, 13 Oct 2026 01:02:56 +0200 Subject: [PATCH 03/81] test(shader): preserve specialized words when optimization is disabled --- core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp index c511be056e4..347cf811433 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp @@ -164,6 +164,8 @@ void testNarrowConversions(bool optimizationEnabled) { check(opcodeCount(normalized, spv::OpStore) == (vector ? 1u : 2u), label + ": optimizer lost buffer stores"); if (!optimizationEnabled) { check(output == input, label + ": none mode changed the shader"); + const auto specialized = ShaderRecompiler::ValidateAndOptimizeSpirv(input, Vulkan11, Spirv13, false, true, true); + check(specialized == input, label + ": none mode changed the specialized shader"); } else { check(opcodeCount(normalized, spv::OpIAdd) == 0, label + ": optimizer did not remove dead arithmetic"); if (arithmetic && !type.floating) { From 1258d354513e24d4b9a6c0c86a7040d6da76cd79 Mon Sep 17 00:00:00 2001 From: dantdmdarealguy Date: Fri, 9 Oct 2026 18:31:18 -0500 Subject: [PATCH 04/81] docs(input): describe the forwarded touchpad fingers and the View button --- docs/user/INPUT_MAPPING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index 83876a67d34..8b8041f3c05 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -2,7 +2,7 @@ AnyPS5 accepts SDL-mapped game controllers as well as its built-in keyboard and mouse bindings. Xbox-style controllers and PlayStation controllers recognized by SDL use the standard PS button layout; sticks and analog triggers are passed through, and controllers can be connected or disconnected while the game is running. The first recognized controller is used. AnyPS5 uses its built-in keyboard and mouse bindings when no configuration file is present. To change selected keyboard and mouse bindings, create `anyps5-input.ini` beside the generated game executable. Set `ANYPS5_INPUT_CONFIG` to use a file at another path. -For an Xbox-layout controller, A/B/X/Y map to Cross/Circle/Square/Triangle, LB/RB map to L1/R1, and LT/RT map to analog L2/R2. Start maps to Options. D-pad and stick-click buttons are supported. On a PlayStation controller, the touchpad click button is forwarded; finger touch coordinates are not implemented. +For an Xbox-layout controller, A/B/X/Y map to Cross/Circle/Square/Triangle, LB/RB map to L1/R1, and LT/RT map to analog L2/R2. Start maps to Options. D-pad and stick-click buttons are supported. On a PlayStation controller, touchpad clicks and the positions of up to two fingers on the touchpad are forwarded. On other controllers, View (Back) presses the touchpad at its center. Each non-empty line has the form `Action = Type:Value`. Action names are case-insensitive. A `#` or `;` starts a comment. The first line for an action replaces its built-in bindings; later lines for the same action add alternate inputs. Duplicate entries for the same action are ignored. Actions omitted from the file keep their built-in bindings. UTF-8 files with or without a byte-order mark are supported. From 7822535b491764f6bb06f204d1b4691df664ed28 Mon Sep 17 00:00:00 2001 From: Zurisen Date: Tue, 13 Oct 2026 08:50:40 +0200 Subject: [PATCH 05/81] fix(progress): extract history archives on Python without tarfile filters progress_history passed filter="data" to TarFile.extractall, which only exists from Python 3.12 and the 3.11.4/3.10.12 security releases. On Python 3.10.11 every measured commit raised TypeError, so progress_history failed. Pass the filter when tarfile has data_filter and extract without it otherwise; the archive is git archive output of the repository's own commits. --- tools/progress_history.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/progress_history.py b/tools/progress_history.py index 26e1ae7b4b8..244b8613111 100644 --- a/tools/progress_history.py +++ b/tools/progress_history.py @@ -34,7 +34,11 @@ def measure(sha): with tempfile.TemporaryDirectory() as directory: root = Path(directory) try: - tarfile.open(fileobj=io.BytesIO(git("archive", sha, "--", *paths))).extractall(root, filter="data") + archive = tarfile.open(fileobj=io.BytesIO(git("archive", sha, "--", *paths))) + if hasattr(tarfile, "data_filter"): + archive.extractall(root, filter="data") + else: + archive.extractall(root) except subprocess.CalledProcessError: return None saved = progress.PRX, progress.OPCODES From 99ae2e812b20d9f3d6f84685948d07de3ee45dbe Mon Sep 17 00:00:00 2001 From: krishalivaibhav Date: Tue, 13 Oct 2026 13:04:10 +0530 Subject: [PATCH 06/81] fix(libc): preserve path services through process exit --- core/libs/prx/libc/CMakeLists.txt | 5 ++ core/libs/prx/libc/src/General.cpp | 9 ++- core/libs/tests/GuestPathLifetime.cpp | 80 +++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 core/libs/tests/GuestPathLifetime.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index fb689577fc7..8288816eed8 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -500,6 +500,11 @@ if(BUILD_TESTING) target_include_directories(guest_exit_order_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_exit_order_tests PRIVATE libc) add_test(NAME guest_exit_order COMMAND guest_exit_order_tests) + add_test_executable(guest_path_lifetime_tests ${LIBS_INCLUDE_DIR}/tests/GuestPathLifetime.cpp) + target_include_directories(guest_path_lifetime_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_path_lifetime_tests PRIVATE libc) + add_test(NAME guest_path_lifetime COMMAND guest_path_lifetime_tests) + set_tests_properties(guest_path_lifetime PROPERTIES TIMEOUT 20) if(NOT APPLE) enable_language(C) add_test_executable(exception_c_cleanup_tests ${LIBS_INCLUDE_DIR}/tests/ExceptionCCleanup.cpp ${LIBS_INCLUDE_DIR}/tests/ExceptionCCleanup.c) diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 133196f578e..ae3dfdfb555 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -42,8 +42,8 @@ struct PathAliases { }; PathAliases& Aliases() { - static PathAliases aliases; - return aliases; + static auto* aliases = new PathAliases; + return *aliases; } std::string TrimSlashes(const char* path) { @@ -117,7 +117,10 @@ struct WorkingDirectory { const std::filesystem::path root = std::filesystem::canonical(std::filesystem::current_path()); std::filesystem::path current = root; }; -WorkingDirectory& Directories() { static WorkingDirectory state; return state; } +WorkingDirectory& Directories() { + static auto* state = new WorkingDirectory; + return *state; +} std::filesystem::path HostPath(WorkingDirectory& state, const std::filesystem::path& guest) { if (auto aliased = ResolveAlias(guest.relative_path().generic_string())) return *aliased; return ResolveHostPath(state.root, guest.relative_path()).make_preferred(); diff --git a/core/libs/tests/GuestPathLifetime.cpp b/core/libs/tests/GuestPathLifetime.cpp new file mode 100644 index 00000000000..44e8f59072d --- /dev/null +++ b/core/libs/tests/GuestPathLifetime.cpp @@ -0,0 +1,80 @@ +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" char* APS5_VABI getcwd_nid_postfix(char*, std::size_t); + +namespace { + +int events[2]; +char expectedRoot[4096]; + +void CheckPathsDuringExit() { + bool passed = false; + try { + std::thread worker([&] { + try { + const std::filesystem::path root(expectedRoot); + char cwd[32] = {}; + passed = getcwd_nid_postfix(cwd, sizeof(cwd)) == cwd && + std::strcmp(cwd, "/") == 0 && + ResolvePath_nid_no_patch(".").lexically_relative(root) == "." && + ResolvePath_nid_no_patch("/").lexically_relative(root) == "." && + ResolvePath_nid_no_patch("/lifetime_alias") == root; + RemovePathAlias_nid_no_patch("/lifetime_alias"); + AddPathAlias_nid_no_patch("/lifetime_alias", expectedRoot); + passed = passed && ResolvePath_nid_no_patch("/lifetime_alias") == root; + } catch (const std::exception& error) { + std::fprintf(stderr, "path services during exit: %s\n", error.what()); + passed = false; + } catch (...) { passed = false; } + }); + worker.join(); + } catch (...) { passed = false; } + const char result = passed ? 'P' : 'F'; + static_cast(!write(events[1], &result, 1)); +} + +bool PathsSurviveExit() { + if (pipe(events) != 0) return false; + const pid_t child = fork(); + if (child < 0) { + close(events[0]); + close(events[1]); + return false; + } + if (child == 0) { + close(events[0]); + const auto root = std::filesystem::canonical(std::filesystem::current_path()).string(); + if (root.size() >= sizeof(expectedRoot)) std::_Exit(2); + std::memcpy(expectedRoot, root.c_str(), root.size() + 1); + if (std::atexit(CheckPathsDuringExit) != 0) std::_Exit(2); + ResolvePath_nid_no_patch("/"); + AddPathAlias_nid_no_patch("/lifetime_alias", expectedRoot); + LibcExit_nid_no_patch(0); + } + close(events[1]); + char result = 0; + const ssize_t count = read(events[0], &result, 1); + close(events[0]); + int status = 0; + return waitpid(child, &status, 0) == child && WIFEXITED(status) && + WEXITSTATUS(status) == 0 && count == 1 && result == 'P'; +} + +} + +int main() { + if (!PathsSurviveExit()) { + std::fputs("guest path services must remain available during process exit\n", stderr); + return 1; + } + return 0; +} From 72fae20877c5ff36c87dc05b704dcc3b2e57aa5f Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Mon, 12 Oct 2026 14:26:50 +0200 Subject: [PATCH 07/81] fix(libSceAudioOut): report the port's channel count from sceAudioOut2PortGetState sceAudioOut2PortGetState returned the two channels of the host output for every port. It now reports the channel count of the port's data format and clears the rest of the state, so the reserved fields no longer keep the caller's stack contents. The speaker info and system state calls are unchanged; a new test covers all three, since none had one. The channel count follows SharpEmu, another emulator; no console measurement exists, recorded in TechnicalDebt.md. ASTRO BOT and ASTRO's PLAYROOM reportedly stop on an assertion that depends on the reported state (#4317); which field matters is not isolated. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAudioOut/CMakeLists.txt | 9 ++ .../prx/libSceAudioOut/src/AudioOut2Port.cpp | 8 +- .../tests/AudioOut2DeviceState.cpp | 137 ++++++++++++++++++ docs/dev/debt/libSceAudioOut.md | 1 + 4 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 core/libs/prx/libSceAudioOut/tests/AudioOut2DeviceState.cpp diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index e499cbf081b..df82deece1a 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -105,6 +105,15 @@ if(BUILD_TESTING) set_tests_properties(audio_out2_port_buffers PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(audio_out2_device_state_tests tests/AudioOut2DeviceState.cpp) + target_include_directories(audio_out2_device_state_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out2_device_state_tests PRIVATE libSceAudioOut libc) + configure_windows_unwind(audio_out2_device_state_tests) + add_test(NAME audio_out2_device_state COMMAND audio_out2_device_state_tests) + if(WIN32) + set_tests_properties(audio_out2_device_state PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(audio_out2_timing_tests tests/AudioOut2Timing.cpp) target_include_directories(audio_out2_timing_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(audio_out2_timing_tests PRIVATE libSceAudioOut libc) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp index 0deb7d563b8..4cb90e2227b 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp @@ -223,13 +223,13 @@ int APS5_VABI sceAudioOut2PortDestroy(AudioOut2PortHandle port) { int APS5_VABI sceAudioOut2PortGetState(AudioOut2PortHandle port, AudioOut2PortState* state) { std::lock_guard lock(g_portsLock); - if (!FromHandle(port)) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; + const auto* entry = FromHandle(port); + if (!entry) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; if (!state) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT; + *state = AudioOut2PortState{}; state->output = OUTPUT_MAIN; - state->num_channels = AUDIO_OUT2_OUTPUT_CHANNELS; + state->num_channels = static_cast(entry->channels); state->volume = VOLUME_MAX; - state->reroute_counter = 0; - state->flags = 0; return 0; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2DeviceState.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2DeviceState.cpp new file mode 100644 index 00000000000..cfa5a7a0620 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2DeviceState.cpp @@ -0,0 +1,137 @@ +#include "SceTypes.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAudioOut2ContextResetParam(AudioOut2ContextParam*); +int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam*, void*, std::size_t, AudioOut2ContextHandle*); +int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle); +int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle, const AudioOut2PortParam*, AudioOut2PortHandle*); +int APS5_VABI sceAudioOut2PortDestroy(AudioOut2PortHandle); +int APS5_VABI sceAudioOut2PortGetState(AudioOut2PortHandle, AudioOut2PortState*); +int APS5_VABI sceAudioOut2GetSpeakerInfo(AudioOut2SpeakerInfo*, std::uint32_t); +int APS5_VABI sceAudioOut2GetSystemState(AudioOut2SystemState*); +} + +static void Require(bool value) { if (!value) std::abort(); } + +namespace { + +constexpr int errorInvalidArgument = static_cast(0x80260502); +constexpr int errorInvalidHandle = static_cast(0x80260503); +constexpr std::uint16_t portTypeMain = 0; +constexpr std::uint16_t outputMain = 1; +constexpr std::int16_t volumeMax = 127; +constexpr std::uint32_t formatFloat = 0; +constexpr std::uint32_t formatS16 = 1; +constexpr std::uint32_t frequency = 48000; + +std::uint32_t Format(std::uint32_t channels, std::uint32_t type) { + return channels << 8 | type; +} + +void SetEnvironment(const char* name, const std::string& value) { +#ifdef _WIN32 + _putenv_s(name, value.c_str()); +#else + setenv(name, value.c_str(), 1); +#endif +} + +AudioOut2PortState StateOfPort(AudioOut2ContextHandle context, std::uint32_t format) { + AudioOut2PortParam params{}; + params.port_type = portTypeMain; + params.data_format = format; + params.sampling_freq = frequency; + AudioOut2PortHandle port = 0; + Require(sceAudioOut2PortCreate(context, ¶ms, &port) == 0); + AudioOut2PortState state; + std::memset(&state, 0xAB, sizeof(state)); + Require(sceAudioOut2PortGetState(port, &state) == 0); + Require(sceAudioOut2PortDestroy(port) == 0); + return state; +} + +void PortStateFollowsTheDataFormat(AudioOut2ContextHandle context) { + struct Case { + std::uint32_t channels; + std::uint32_t sampleType; + }; + for (const Case entry : {Case{1, formatFloat}, Case{2, formatFloat}, Case{2, formatS16}, Case{8, formatFloat}, Case{12, formatS16}}) { + const auto state = StateOfPort(context, Format(entry.channels, entry.sampleType)); + Require(state.output == outputMain); + Require(state.num_channels == entry.channels); + Require(state.volume == volumeMax); + Require(state.reroute_counter == 0); + Require(state.flags == 0); + Require(state.pad1 == 0 && state.pad2 == 0); + for (const auto word : state.reserved) Require(word == 0); + } +} + +void PortStateRejectsBadArguments(AudioOut2ContextHandle context) { + AudioOut2PortState state{}; + Require(sceAudioOut2PortGetState(0, &state) == errorInvalidHandle); + Require(sceAudioOut2PortGetState(0x7fff, &state) == errorInvalidHandle); + + AudioOut2PortParam params{}; + params.port_type = portTypeMain; + params.data_format = Format(2, formatFloat); + params.sampling_freq = frequency; + AudioOut2PortHandle port = 0; + Require(sceAudioOut2PortCreate(context, ¶ms, &port) == 0); + Require(sceAudioOut2PortGetState(port, nullptr) == errorInvalidArgument); + Require(sceAudioOut2PortDestroy(port) == 0); + Require(sceAudioOut2PortGetState(port, &state) == errorInvalidHandle); +} + +void SpeakerInfoReportsTheFrontPair() { + AudioOut2SpeakerInfo info; + std::memset(&info, 0xAB, sizeof(info)); + Require(sceAudioOut2GetSpeakerInfo(&info, 0) == 0); + Require(info.type == 0); + Require(info.available_bits == 0x3); + Require(info.flags == 0); + Require(info.speaker_angle[0].azimuth == -30 && info.speaker_angle[0].elevation == 0); + Require(info.speaker_angle[1].azimuth == 30 && info.speaker_angle[1].elevation == 0); + for (int index = 2; index < 16; index++) Require(info.speaker_angle[index].azimuth == 0 && info.speaker_angle[index].elevation == 0); + Require(sceAudioOut2GetSpeakerInfo(nullptr, 0) == errorInvalidArgument); +} + +void SystemStateIsSilentAndRejectsNull() { + AudioOut2SystemState state; + std::memset(&state, 0xAB, sizeof(state)); + Require(sceAudioOut2GetSystemState(&state) == 0); + Require(state.loudness == 0.0f); + Require(sceAudioOut2GetSystemState(nullptr) == errorInvalidArgument); +} + +} + +int main() { + const auto path = std::filesystem::temp_directory_path() / ("anyps5_audio_out2_device_state-" + std::to_string(std::random_device{}()) + ".raw"); + std::filesystem::remove(path); + SetEnvironment("SDL_DISKAUDIOFILE", path.string()); + + AudioOut2ContextParam params{}; + Require(sceAudioOut2ContextResetParam(¶ms) == 0); + params.num_grains = 256; + params.queue_depth = 1; + AudioOut2ContextHandle context = 0; + Require(sceAudioOut2ContextCreate(¶ms, nullptr, 0, &context) == 0); + + PortStateFollowsTheDataFormat(context); + PortStateRejectsBadArguments(context); + SpeakerInfoReportsTheFrontPair(); + SystemStateIsSilentAndRejectsNull(); + + Require(sceAudioOut2ContextDestroy(context) == 0); + std::filesystem::remove(path); + return 0; +} diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 6a7069d8cb5..f0a034dbdc9 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -17,6 +17,7 @@ - [sceAudioOut2PortCreate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - the stereo fold is assumed, not the console's downmix: C at -3 dB into both sides, Ls/Rs, Lb/Rb and Ltf/Rtf at -3 dB and Ltb/Rtb at -6 dB into their own side, LFE dropped - [sceAudioOut2PortSetAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - each PCM buffer set (attribute 0) is taken as one grain: buffers set between two pushes are queued and played one per grain, at most 16 queued. A grain with no new buffer repeats the last one only when the port has always been given that same buffer (rewritten in place); a port whose buffers move on plays silence instead. Inferred from PPSA12544, whose object port (type 6) is given one grain of a 64-grain ring per set, often two or three between pushes and then none for a few; the console behaviour for several sets per push and for a push without a new buffer is not documented - [sceAudioOutDeviceIdOpen](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA01317, which fails to load without it; declared without parameters, signature unknown. It opens a port on a given audio device; which device ids exist and how they map to host outputs is unknown, so it throws +- [sceAudioOut2PortGetState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) and [sceAudioOut2GetSpeakerInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp) (libSceAudioOut) - the port state reports the channel count of the port's data format, output main, volume 127 and a zero reroute counter and flags; the speaker info reports a stereo front pair at -30 and +30 degrees. Taking the channel count from the data format follows [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs), another emulator, and no console measurement exists for either call: the output of a pad port, the volume after `sceAudioOut2PortSetAttributes` and the reroute counter on a device change are unverified, as are the speaker types other than stereo. ASTRO BOT and ASTRO's PLAYROOM reportedly stop on an assertion that depends on these values (#4317), but which field matters has not been isolated ### Functional From 149aea506152466d0f0befa14c747105ab29f810 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 12 Oct 2026 15:44:06 +0200 Subject: [PATCH 08/81] fix(stubgen): clean read-only staged files after failure --- tools/stubgen.py | 6 +++++- tools/tests/test_stubgen.py | 39 +++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/tools/stubgen.py b/tools/stubgen.py index 492f485b532..e0fa04f2771 100644 --- a/tools/stubgen.py +++ b/tools/stubgen.py @@ -197,7 +197,11 @@ def write_outputs(outputs): temporary.replace(path) finally: for temporary, path in staged: - temporary.unlink(missing_ok=True) + try: + temporary.unlink(missing_ok=True) + except PermissionError: + temporary.chmod(0o600) + temporary.unlink(missing_ok=True) def main(argv=None): diff --git a/tools/tests/test_stubgen.py b/tools/tests/test_stubgen.py index e0d0cde7c76..ab0fdd2d6d3 100644 --- a/tools/tests/test_stubgen.py +++ b/tools/tests/test_stubgen.py @@ -1,6 +1,7 @@ import contextlib import io import os +import stat import tempfile from pathlib import Path import sys @@ -121,5 +122,43 @@ def test_valid_generation_keeps_the_code_and_debt(self): self.assertEqual(code.stat().st_mode & 0o777, 0o666 & ~mask) +class ReadOnlyOutputTests(unittest.TestCase): + def test_later_staging_failure_preserves_original_error_and_cleans_readonly_temporary(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + nids, output = root / "nids.txt", root / "Export.cpp" + nids.write_text("AAAAAAAAAAA\n", encoding="utf-8") + original = "existing source\n" + output.write_text(original, encoding="utf-8") + output.chmod(0o444) + errors = io.StringIO() + arguments = ["--module", "libSceExample", "--nids", str(nids), "--out", str(output), + "--td-out", str(root / "absent" / "debt.txt")] + with contextlib.redirect_stderr(errors), self.assertRaises(SystemExit) as error: + stubgen.main(arguments) + self.assertEqual(error.exception.code, 2) + self.assertIn("absent", errors.getvalue()) + self.assertNotIn("Export.cpp.", errors.getvalue()) + self.assertEqual(output.read_text(encoding="utf-8"), original) + self.assertFalse(output.stat().st_mode & stat.S_IWRITE) + self.assertEqual(sorted(path.name for path in root.iterdir()), ["Export.cpp", "nids.txt"]) + + @unittest.skipUnless(os.name == "nt", "Windows denies replacing read-only output files") + def test_denied_replace_preserves_readonly_output_without_leaking_temporary(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + nids, output = root / "nids.txt", root / "Export.cpp" + nids.write_text("AAAAAAAAAAA\n", encoding="utf-8") + original = "existing source\n" + output.write_text(original, encoding="utf-8") + output.chmod(0o444) + with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as error: + stubgen.main(["--module", "libSceExample", "--nids", str(nids), "--out", str(output)]) + self.assertEqual(error.exception.code, 2) + self.assertEqual(output.read_text(encoding="utf-8"), original) + self.assertFalse(output.stat().st_mode & stat.S_IWRITE) + self.assertEqual(sorted(path.name for path in root.iterdir()), ["Export.cpp", "nids.txt"]) + + if __name__ == "__main__": unittest.main() From 8a51ff3154dd322b8e4e7f33d39671f11fb61759 Mon Sep 17 00:00:00 2001 From: ideabsassistenza Date: Tue, 13 Oct 2026 10:03:49 +0200 Subject: [PATCH 09/81] fix(tests): ignore bracket comments in registration audit --- tools/check_test_registration.py | 4 ++++ tools/tests/test_test_registration.py | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index 0fb47521a60..8c2bc8bd74c 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -6,6 +6,8 @@ TEST_OUTPUT = r"tests[\\/]" COMMAND = re.compile(r"^\s*(add_test|set_tests_properties|subdirs)\s*\(", re.M | re.I) ARGUMENT = re.compile(r'"(?:\\.|[^"\\])*"|\[(?P=*)\[.*?\](?P=equals)\]|[^\s()]+', re.S) +LEXICAL = re.compile(r'\\.|"(?:\\.|[^"\\])*"|\[(?P=*)\[.*?\](?P=literal_equals)\]' + r'|(?P\#\[(?P=*)\[.*?\](?P=comment_equals)\]|\#[^\n]*)', re.S) KNOWN_UNRUN = set() @@ -30,6 +32,8 @@ def built(build): def commands(content): + content = LEXICAL.sub(lambda match: re.sub(r"[^\n]", " ", match.group()) + if match.group("comment") is not None else match.group(), content) parsed_until = 0 for command in COMMAND.finditer(content): if command.start() < parsed_until: diff --git a/tools/tests/test_test_registration.py b/tools/tests/test_test_registration.py index 3d91bf0d65a..40234b6252b 100644 --- a/tools/tests/test_test_registration.py +++ b/tools/tests/test_test_registration.py @@ -16,6 +16,28 @@ def registered(self, content): (build / "CTestTestfile.cmake").write_text(content, encoding="utf-8") return check_test_registration.registered(build) + def test_bracket_comments_do_not_register_tests(self): + for equals in ("", "=", "==="): + with self.subTest(equals=equals): + content = (f"#[{equals}[\nadd_test(ghost /build/tests/missing_tests)\n]{equals}]\n" + 'add_test("real" "/build/tests/real_tests")\n') + self.assertEqual(self.registered(content), {"real_tests"}) + + def test_bracket_comment_does_not_disable_a_registered_test(self): + content = ('add_test("real" "/build/tests/real_tests")\n' + '#[=[\nset_tests_properties("real" PROPERTIES DISABLED TRUE)\n]=]\n') + self.assertEqual(self.registered(content), {"real_tests"}) + + def test_bracket_comment_inside_arguments_does_not_end_the_command(self): + content = ('add_test("real" #[=[ )\nadd_test(ghost /build/tests/missing_tests)\n]=]\n' + '"/build/tests/real_tests")\n') + self.assertEqual(self.registered(content), {"real_tests"}) + + def test_comment_markers_inside_literals_remain_arguments(self): + content = ('add_test("real" "/build/tests/real_tests" "#[[" [=[# comment]=] "\\\\#")\n') + self.assertEqual(list(check_test_registration.commands(content)), + [("add_test", ["real", "/build/tests/real_tests", "#[[", "# comment", "\\#"])]) + def test_test_name_does_not_register_executable(self): names = self.registered('add_test("missing_tests" "/usr/bin/python3" "unrelated.py")\n') self.assertNotIn("missing_tests", names) From 59fd2f7968d46c65f80e4245af6bc6af93c25d0c Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Tue, 13 Oct 2026 04:29:09 +0000 Subject: [PATCH 10/81] fix(libSceSystemService): include rejected LoadExec path --- core/libs/tests/GuestSystemExit.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/tests/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp index f0d40600b18..faa0740a86e 100644 --- a/core/libs/tests/GuestSystemExit.cpp +++ b/core/libs/tests/GuestSystemExit.cpp @@ -173,7 +173,7 @@ int main(int argc, char** argv) { for (const char* path : {"/app0/another.bin", "/data/eboot.bin", "/app0/../eboot.bin", "/app0//eboot.bin"}) { bool rejected = false; try { sceSystemServiceLoadExec(path, nullptr); } - catch (const std::runtime_error&) { rejected = true; } + catch (const std::runtime_error& error) { rejected = std::string(error.what()).find(path) != std::string::npos; } Require(rejected); } #ifdef _WIN32 From 92618d25c387597b05e00de181552c0281712bc1 Mon Sep 17 00:00:00 2001 From: SandstromPL Date: Tue, 13 Oct 2026 00:48:06 +0530 Subject: [PATCH 11/81] fix(relinker): replace output files instead of rewriting them in place Co-Authored-By: Claude Opus 5.5 --- core/relinker/io/src/FileWriter.cpp | 67 ++++++++-- core/relinker/io/tests/FileWriterTests.cpp | 142 ++++++++++++++++++++- 2 files changed, 193 insertions(+), 16 deletions(-) diff --git a/core/relinker/io/src/FileWriter.cpp b/core/relinker/io/src/FileWriter.cpp index e80448f5036..1760af8f974 100644 --- a/core/relinker/io/src/FileWriter.cpp +++ b/core/relinker/io/src/FileWriter.cpp @@ -1,28 +1,73 @@ #include #include #include +#include #include +#include namespace Io { -void FileWriter::Write(const std::string& path, const std::vector& data) { - std::ofstream f(NativePath(path), std::ios::binary); +namespace { + +template +bool writeFile(const std::filesystem::path& file, const std::string& path, const std::ios::openmode mode, const TFill& fill) { + std::ofstream f(file, mode); if (!f) - throw Domain::RelinkerException("Cannot open output file: " + path); - f.write(reinterpret_cast(data.data()), static_cast(data.size())); + return false; + fill(f); f.close(); if (!f) throw Domain::RelinkerException("Failed to write file: " + path); + return true; } -void FileWriter::Write(const std::string& path, const std::string& content) { - std::ofstream f(NativePath(path)); - if (!f) +#ifndef _WIN32 +bool moveOver(const std::filesystem::path& temporary, const std::filesystem::path& target, const std::filesystem::file_status& previous) { + std::error_code error; + if (std::filesystem::is_regular_file(previous)) + std::filesystem::permissions(temporary, previous.permissions() & std::filesystem::perms::all, error); + if (!error) + std::filesystem::rename(temporary, target, error); + if (!error) + return true; + std::filesystem::remove(temporary, error); + return false; +} +#endif + +template +void replaceFile(const std::string& path, const std::ios::openmode mode, const TFill& fill) { + const auto target = NativePath(path); +#ifndef _WIN32 + std::error_code error; + const auto previous = std::filesystem::symlink_status(target, error); + auto temporary = target; + temporary += ".tmp"; + if ((std::filesystem::is_regular_file(previous) || previous.type() == std::filesystem::file_type::not_found) + && std::filesystem::symlink_status(temporary, error).type() == std::filesystem::file_type::not_found) { + try { + if (writeFile(temporary, path, mode, fill) && moveOver(temporary, target, previous)) + return; + } catch (...) { + std::filesystem::remove(temporary, error); + throw; + } + } +#endif + if (!writeFile(target, path, mode, fill)) throw Domain::RelinkerException("Cannot open output file: " + path); - f << content; - f.close(); - if (!f) - throw Domain::RelinkerException("Failed to write file: " + path); +} + +} + +void FileWriter::Write(const std::string& path, const std::vector& data) { + replaceFile(path, std::ios::binary, [&data](std::ofstream& f) { + f.write(reinterpret_cast(data.data()), static_cast(data.size())); + }); +} + +void FileWriter::Write(const std::string& path, const std::string& content) { + replaceFile(path, std::ios::out, [&content](std::ofstream& f) { f << content; }); } } diff --git a/core/relinker/io/tests/FileWriterTests.cpp b/core/relinker/io/tests/FileWriterTests.cpp index 8edc710d559..3ca74275984 100644 --- a/core/relinker/io/tests/FileWriterTests.cpp +++ b/core/relinker/io/tests/FileWriterTests.cpp @@ -1,9 +1,18 @@ #include #include +#include +#include +#include #include +#include #include #include +#include #include +#include +#include +#include +#include namespace { @@ -21,6 +30,124 @@ void requireWriteFailure(const TValue& data) { throw std::runtime_error("Write to /dev/full reported success"); } +std::string readFile(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +template +void requireRunningReplacement(const std::filesystem::path& directory, const TValue& data) { + const auto output = directory / "running"; + std::filesystem::copy_file("/bin/sleep", output); + const auto permissions = std::filesystem::perms::owner_all | std::filesystem::perms::group_read | std::filesystem::perms::group_exec; + std::filesystem::permissions(output, permissions); + const auto previous = readFile(output); + const pid_t child = fork(); + if (child < 0) + throw std::runtime_error("Cannot start the previous executable"); + if (child == 0) { + execl(output.c_str(), "sleep", "30", nullptr); + _exit(127); + } + std::error_code error; + bool started = false; + for (int attempt = 0; attempt < 2000 && !started; ++attempt) { + started = std::filesystem::read_symlink("/proc/" + std::to_string(child) + "/exe", error) == output; + if (!started) + usleep(1000); + } + const int descriptor = open(output.c_str(), O_RDONLY); + std::string failure; + if (started) { + try { + Io::FileWriter().Write(output.string(), data); + } catch (const std::exception& e) { + failure = e.what(); + } + } + std::string kept(previous.size(), '\0'); + const bool keptPrevious = pread(descriptor, kept.data(), kept.size(), 0) == static_cast(kept.size()) && kept == previous; + close(descriptor); + const bool running = waitpid(child, nullptr, WNOHANG) == 0; + if (running) { + kill(child, SIGKILL); + waitpid(child, nullptr, 0); + } + if (!started) + throw std::runtime_error("The previous executable did not start"); + if (!failure.empty()) + throw std::runtime_error("Replacing a running executable failed: " + failure); + if (readFile(output) != std::string(data.begin(), data.end())) + throw std::runtime_error("Running executable was not replaced"); + if ((std::filesystem::status(output).permissions() & std::filesystem::perms::all) != permissions) + throw std::runtime_error("Replacement did not keep the previous permissions"); + if (!keptPrevious) + throw std::runtime_error("An open descriptor no longer reads the previous file"); + if (!running) + throw std::runtime_error("Running executable stopped"); + if (std::filesystem::exists(directory / "running.tmp")) + throw std::runtime_error("Temporary file left behind"); +} + +template +void requireFailedReplacement(const std::filesystem::path& directory, const TValue& data) { + const auto output = directory / "limited"; + std::ofstream(output, std::ios::binary) << "previous"; + rlimit limit{}; + getrlimit(RLIMIT_FSIZE, &limit); + rlimit lowered = limit; + lowered.rlim_cur = 1; + const auto handler = std::signal(SIGXFSZ, SIG_IGN); + setrlimit(RLIMIT_FSIZE, &lowered); + std::string failure; + try { + Io::FileWriter().Write(output.string(), data); + } catch (const Domain::RelinkerException& e) { + failure = e.what(); + } + setrlimit(RLIMIT_FSIZE, &limit); + std::signal(SIGXFSZ, handler); + if (failure != "Failed to write file: " + output.string()) + throw std::runtime_error("Write past the file size limit did not fail: " + failure); + if (readFile(output) != "previous" || std::filesystem::exists(directory / "limited.tmp")) + throw std::runtime_error("Failed write did not keep the previous file"); +} + +template +void requireInPlaceWrites(const std::filesystem::path& directory, const TValue& data) { + const std::string expected(data.begin(), data.end()); + const auto target = directory / "target"; + const auto link = directory / "link"; + std::ofstream(target, std::ios::binary) << "previous"; + std::filesystem::create_symlink(target, link); + Io::FileWriter().Write(link.string(), data); + if (!std::filesystem::is_symlink(link) || readFile(target) != expected) + throw std::runtime_error("Symbolic link output was not written through"); + const auto reserved = directory / "reserved"; + std::ofstream(reserved, std::ios::binary) << "previous"; + std::ofstream(directory / "reserved.tmp", std::ios::binary) << "unrelated"; + Io::FileWriter().Write(reserved.string(), data); + if (readFile(reserved) != expected || readFile(directory / "reserved.tmp") != "unrelated") + throw std::runtime_error("Existing temporary file name was not left alone"); +} + +template +void requireReplacement(const TValue& data) { + const auto directory = std::filesystem::canonical(std::filesystem::temp_directory_path()) / ("anyps5-file-writer-" + std::to_string(getpid())); + std::error_code error; + std::filesystem::remove_all(directory, error); + std::filesystem::create_directory(directory); + try { + requireRunningReplacement(directory, data); + requireFailedReplacement(directory, data); + requireInPlaceWrites(directory, data); + } catch (...) { + std::filesystem::remove_all(directory, error); + throw; + } + std::filesystem::remove_all(directory, error); +} + } int main(int argc, char* argv[]) { @@ -28,12 +155,17 @@ int main(int argc, char* argv[]) { if (argc != 2) throw std::runtime_error("Expected binary or text"); const std::string mode = argv[1]; for (const std::size_t size : {std::size_t{3}, std::size_t{65536}}) { - if (mode == "binary") - requireWriteFailure(std::vector(size, 0x41)); - else if (mode == "text") - requireWriteFailure(std::string(size, 'A')); - else + if (mode == "binary") { + const std::vector data(size, 0x41); + requireWriteFailure(data); + requireReplacement(data); + } else if (mode == "text") { + const std::string data(size, 'A'); + requireWriteFailure(data); + requireReplacement(data); + } else { throw std::runtime_error("Unknown test mode: " + mode); + } } } catch (const std::exception& e) { std::cerr << e.what() << '\n'; From 08bd00c8cf0fc6389f35b5a7ffddb67f1e929100 Mon Sep 17 00:00:00 2001 From: sussy Date: Tue, 13 Oct 2026 12:00:24 +0300 Subject: [PATCH 12/81] docs(build): link the supported WinLibs release --- docs/dev/BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 82b55767011..c7c34dcc162 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -30,7 +30,7 @@ git submodule update --init --recursive - Linux display refresh queries require SDL's XRandR support (`libxrandr-dev` on Debian/Ubuntu; `libXrandr-devel` on Fedora). Bundled SDL configuration fails without it. Keep `SDL_X11_XRANDR` enabled. - Linux: GCC, G++, binutils. SDL's X11 backend requires X11 and Xext development headers (`libx11-dev` and `libxext-dev` on Debian/Ubuntu). Audio output needs SDL's PipeWire, PulseAudio or ALSA backend, built from their development files (`libpipewire-0.3-dev`, `libpulse-dev` and `libasound2-dev` on Debian/Ubuntu; `pipewire-devel`, `pulseaudio-libs-devel` and `alsa-lib-devel` on Fedora). SDL loads them at run time. Without any of them SDL keeps only its silent dummy and disk drivers, so configuration fails. - Fedora: if CMake reports `Could NOT find Threads`, install `glibc-static` and `libstdc++-static` with `sudo dnf install glibc-static libstdc++-static`, then rerun configuration. The project enables static executable linking, and FFmpeg's thread check needs these libraries. -- Windows: only MinGW-w64 GCC 15.2.0 (WinLibs `x86_64-ucrt-posix-seh`, release `15.2.0posix-14.0.0-ucrt-r7`) is currently supported. Add its `mingw64/bin` directory to `PATH` before configuring. +- Windows: only MinGW-w64 GCC 15.2.0 (WinLibs `x86_64-ucrt-posix-seh`, release [`15.2.0posix-14.0.0-ucrt-r7`](https://github.com/brechtsanders/winlibs_mingw/releases/tag/15.2.0posix-14.0.0-ucrt-r7), archive `winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip`) is currently supported. Add its `mingw64/bin` directory to `PATH` before configuring. - Windows antivirus software can quarantine the executables that the relinker tests create in the temp directory, which fails `optional_plt`, `empty_tls`, `windows_address_space` and `windows_icon`. Set `TEMP` and `TMP` to a directory the scanner excludes before running `ctest`. - FFmpeg binaries are downloaded during configuration unless `FFMPEG_PREBUILT_DIR` is set. With the WinLibs CMake, the download fails with status 60 (`SSL peer certificate or SSH remote key was not OK`) unless `SSL_CERT_FILE` names a CA bundle, for example `C:\Program Files\Git\mingw64\etc\ssl\certs\ca-bundle.crt` from Git for Windows, as in CI. From 43102202c3256bdd2321b62e86eb30df4a7ea8d8 Mon Sep 17 00:00:00 2001 From: screamm Date: Tue, 13 Oct 2026 11:20:59 +0200 Subject: [PATCH 13/81] docs(user): stage the original eboot.bin in app0 Four catalog titles (ProsperoTV PPSA99003, ProsperoRadio PPSA99001, ProsperoLight PPSA99002, ProsperoLichess PPSA99009) locate their own directory with stat("/app0/eboot.bin"), which always exists on the console, and fall back to /data/homebrew/ without it; that directory is empty here, so they fail to load their fonts and die with "renderer init failed". Staging the original eboot.bin next to the app resources makes all four load their fonts; ProsperoLichess then renders (1267 frames in 60 s on Windows). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --- docs/user/USAGE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index b4f658a5439..1bb90e97e9b 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -80,11 +80,12 @@ libs/ *.prx app0/ <app resources> + eboot.bin sce_module/ <converted modules> ``` -Use `sce_modules/` or `prx/` instead of `sce_module/` if that is the input directory name. Relinker preserves each module's directory under `app0/` and prints its exact path. Place app resources in `app0/` separately. Copy the built system libraries from `build/core/libs/libs/*.prx` into `libs/`; use libraries built for the target OS. A custom `--rpath` changes the system library location. +Use `sce_modules/` or `prx/` instead of `sce_module/` if that is the input directory name. Relinker preserves each module's directory under `app0/` and prints its exact path. Place app resources in `app0/` separately, including the title's original `eboot.bin`: titles test `/app0/eboot.bin` to find their own directory, as it is always present on the console (ProsperoTV, ProsperoRadio, ProsperoLight and ProsperoLichess fall back to an empty `/data/homebrew/<title id>` and fail to load their fonts without it). Only the converted executable runs. Copy the built system libraries from `build/core/libs/libs/*.prx` into `libs/`; use libraries built for the target OS. A custom `--rpath` changes the system library location. Use the generated files printed as `Guest module:` for bundled title modules. `libs/` is for AnyPS5 system libraries, not the original PS5 `.prx` files. Placing an original PS5 module in `libs/` on Windows makes Windows try to load it as a DLL and can fail with error 193 (not a valid Win32 application). From 59235d9cb0de5c27dcba2160a75188da9b585bf3 Mon Sep 17 00:00:00 2001 From: LeandroLP23 <leandroplacidi@gmail.com> Date: Tue, 13 Oct 2026 15:29:43 +0200 Subject: [PATCH 14/81] test(agc): compile each v_cmp denormals program once for its eight input groups The test recompiled and rebuilt the same program for every group of 32 input pairs. Only the input buffer changes between groups, so compile once per compare, EXEC/VCC capture and float mode (96 programs instead of 768) and dispatch it for each group. On lavapipe the test drops from about 11.5 s to 1.3 s; it is the slowest test of the Linux CI run (14.5 s). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../tests/execution/VCmpF32Denormals.cpp | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32Denormals.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32Denormals.cpp index 54e132b6c6e..22568a2f9ad 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32Denormals.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32Denormals.cpp @@ -95,17 +95,9 @@ std::string Hex(std::uint32_t value) { return text; } -std::uint32_t Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode mode, std::uint32_t compare, std::uint32_t capture, std::uint32_t group) { +ShaderRecompiler::RecompileResult Compile(const AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode mode, std::uint32_t compare, std::uint32_t capture) { Code[6] = compare; Code[7] = capture; - for (std::uint32_t lane = 0; lane < Threads; ++lane) { - const std::uint32_t pair = group * Threads + lane; - Input[lane * Inputs] = Values[pair / ValueCount]; - Input[lane * Inputs + 1u] = Values[pair % ValueCount]; - Input[lane * Inputs + 2u] = 0u; - Input[lane * Inputs + 3u] = 0u; - } - Output.fill(0xdeadbeefu); std::vector<std::uint32_t> userData(8, 0u); const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u)); const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u)); @@ -122,8 +114,19 @@ std::uint32_t Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloat }; request.useCache = false; request.context.floatMode = mode; - const auto result = ShaderRecompiler::Recompile(request); - device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data())); + return ShaderRecompiler::Recompile(request); +} + +std::uint32_t Run(AgcDriver::VulkanDevice& device, const ShaderRecompiler::RecompileResult& program, std::uint32_t group) { + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + const std::uint32_t pair = group * Threads + lane; + Input[lane * Inputs] = Values[pair / ValueCount]; + Input[lane * Inputs + 1u] = Values[pair % ValueCount]; + Input[lane * Inputs + 2u] = 0u; + Input[lane * Inputs + 3u] = 0u; + } + Output.fill(0xdeadbeefu); + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(Code.data())); device.WaitIdle(); std::uint32_t mask = 0u; for (std::uint32_t lane = 0; lane < Threads; ++lane) { @@ -135,8 +138,9 @@ std::uint32_t Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloat void Check(AgcDriver::VulkanDevice& device, const char* modeName, ShaderRecompiler::ShaderFloatMode mode, const Row& row, bool exec, const std::uint32_t* expected) { const std::string name = std::string(exec ? "v_cmpx_" : "v_cmp_") + row.condition + "_f32"; + const auto program = Compile(device, mode, exec ? row.cmpx : row.cmp, exec ? CaptureExec : CaptureVcc); for (std::uint32_t group = 0; group < Groups; ++group) { - const std::uint32_t actual = Run(device, mode, exec ? row.cmpx : row.cmp, exec ? CaptureExec : CaptureVcc, group); + const std::uint32_t actual = Run(device, program, group); if (actual == expected[group]) continue; const std::uint32_t diff = actual ^ expected[group]; for (std::uint32_t lane = 0; lane < Threads; ++lane) { From 3065b892f2e28657fa4adf00593305ed17f9a75d Mon Sep 17 00:00:00 2001 From: Rick <99503318+0xRickx@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:00:35 +0200 Subject: [PATCH 15/81] fix(libSceNetCtl): answer sceNetCtlGetInfo from the host IPv4 interface sceNetCtlGetState reports IPOBTAINED from the host's addresses, but sceNetCtlGetInfo returned NOT_CONNECTED for every code. It now answers from the host interface that holds an IPv4 address, preferring the one with the default route: its address, netmask, gateway, MTU and DNS servers. The link is presented as wired, connected, configured by DHCP and without a proxy, with the Ethernet address sceNetGetMacAddress reports. A null info returns INVALID_ADDR and a code outside 1..21 INVALID_CODE; the Wi-Fi, DHCP host name, PPPoE and proxy server codes throw. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --- core/libs/prx/libSceNetCtl/CMakeLists.txt | 14 ++ core/libs/prx/libSceNetCtl/Export.cpp | 203 +++++++++++++++++++++- core/libs/tests/GuestNetCtl.cpp | 136 +++++++++++++++ docs/dev/debt/libSceNetCtl.md | 5 + 4 files changed, 353 insertions(+), 5 deletions(-) create mode 100644 core/libs/tests/GuestNetCtl.cpp create mode 100644 docs/dev/debt/libSceNetCtl.md diff --git a/core/libs/prx/libSceNetCtl/CMakeLists.txt b/core/libs/prx/libSceNetCtl/CMakeLists.txt index f221f21932c..0083a712aff 100644 --- a/core/libs/prx/libSceNetCtl/CMakeLists.txt +++ b/core/libs/prx/libSceNetCtl/CMakeLists.txt @@ -1,3 +1,17 @@ add_library(libSceNetCtl SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceNetCtl PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceNetCtl) + +if(BUILD_TESTING) + add_test_executable(guest_net_ctl_tests ${LIBS_INCLUDE_DIR}/tests/GuestNetCtl.cpp) + target_include_directories(guest_net_ctl_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_net_ctl_tests PRIVATE libSceNetCtl libc) + configure_windows_unwind(guest_net_ctl_tests) + add_test(NAME guest_net_ctl COMMAND guest_net_ctl_tests) + set_tests_properties(guest_net_ctl PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77) + if(WIN32) + target_link_libraries(guest_net_ctl_tests PRIVATE ws2_32) + set_tests_properties(guest_net_ctl PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNetCtl>") + endif() +endif() diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 5f0ab11e266..332ee323f73 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -4,32 +4,58 @@ #include <ws2tcpip.h> #include <iphlpapi.h> #else +#include <arpa/inet.h> #include <ifaddrs.h> #include <net/if.h> +#include <net/route.h> #include <netinet/in.h> +#include <sys/ioctl.h> +#include <sys/socket.h> +#include <unistd.h> #ifdef __linux__ #include <netpacket/packet.h> #endif #endif #include <cstdint> #include <cstddef> +#include <cstdio> +#include <cstring> #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include <fstream> +#include <memory> #include <mutex> +#include <sstream> #include <stdexcept> #include <string> -#ifdef _WIN32 #include <vector> -#endif // Network-control state reflects host interface addressing, not PSN sign-in status. static constexpr int SCE_NET_CTL_ERROR_CALLBACK_MAX = static_cast<int>(0x80412103); static constexpr int SCE_NET_CTL_ERROR_INVALID_ID = static_cast<int>(0x80412105); +static constexpr int SCE_NET_CTL_ERROR_INVALID_CODE = static_cast<int>(0x80412106); static constexpr int SCE_NET_CTL_ERROR_INVALID_ADDR = static_cast<int>(0x80412107); static constexpr int SCE_NET_CTL_ERROR_NOT_CONNECTED = static_cast<int>(0x80412108); static constexpr int NET_CTL_STATE_DISCONNECTED = 0; static constexpr int NET_CTL_STATE_IPOBTAINED = 3; +static constexpr int NET_CTL_INFO_DEVICE = 1; +static constexpr int NET_CTL_INFO_ETHER_ADDR = 2; +static constexpr int NET_CTL_INFO_MTU = 3; +static constexpr int NET_CTL_INFO_LINK = 4; +static constexpr int NET_CTL_INFO_IP_CONFIG = 11; +static constexpr int NET_CTL_INFO_IP_ADDRESS = 14; +static constexpr int NET_CTL_INFO_NETMASK = 15; +static constexpr int NET_CTL_INFO_DEFAULT_ROUTE = 16; +static constexpr int NET_CTL_INFO_PRIMARY_DNS = 17; +static constexpr int NET_CTL_INFO_SECONDARY_DNS = 18; +static constexpr int NET_CTL_INFO_HTTP_PROXY_CONFIG = 19; +static constexpr int NET_CTL_INFO_HTTP_PROXY_PORT = 21; +static constexpr std::uint32_t NET_CTL_DEVICE_WIRED = 0; +static constexpr std::uint32_t NET_CTL_LINK_CONNECTED = 1; +static constexpr std::uint32_t NET_CTL_IP_DHCP = 0; +static constexpr std::uint32_t NET_CTL_HTTP_PROXY_OFF = 0; +static constexpr std::uint8_t NET_CTL_ETHER_ADDR[6] = {0x02, 0x50, 0x53, 0x35, 0x00, 0x01}; +static constexpr std::size_t NET_CTL_IPV4_ADDR_STR_LEN = 16; static constexpr int MAX_CALLBACKS = 8; static std::mutex g_callbackLock; @@ -84,6 +110,157 @@ static bool HostHasAddress(int family) { #endif } +struct HostIpv4Interface { + in_addr address{}; + in_addr netmask{}; + in_addr gateway{}; + std::uint32_t mtu = 0; + std::vector<in_addr> dns; +}; + +static bool UsableIpv4(in_addr address) { + const auto host = ntohl(address.s_addr); + return host != 0 && (host >> 24) != 127; +} + +#ifdef _WIN32 +static const sockaddr_in* Ipv4Of(const SOCKET_ADDRESS& address) { + if (!address.lpSockaddr || address.lpSockaddr->sa_family != AF_INET) return nullptr; + return reinterpret_cast<const sockaddr_in*>(address.lpSockaddr); +} + +static std::uint32_t PrefixMask(unsigned length) { + if (length == 0) return 0; + return htonl(length >= 32 ? 0xFFFFFFFFu : 0xFFFFFFFFu << (32 - length)); +} +#else +struct HostDefaultRoute { + std::string name; + in_addr gateway{}; + unsigned long metric = 0; +}; + +static std::vector<HostDefaultRoute> HostDefaultRoutes() { + std::ifstream table("/proc/net/route"); + if (!table) throw std::runtime_error("sceNetCtlGetInfo: the host routing table is unavailable"); + std::vector<HostDefaultRoute> routes; + std::string line; + std::getline(table, line); + while (std::getline(table, line)) { + std::istringstream fields(line); + HostDefaultRoute route; + unsigned long destination = 0; + unsigned long gateway = 0; + unsigned long flags = 0; + unsigned long references = 0; + unsigned long uses = 0; + unsigned long mask = 0; + if (!(fields >> route.name >> std::hex >> destination >> gateway >> flags >> std::dec >> references >> uses >> route.metric >> std::hex >> mask)) continue; + if (destination != 0 || mask != 0 || !(flags & RTF_UP) || (flags & RTF_REJECT)) continue; + route.gateway.s_addr = static_cast<std::uint32_t>(gateway); + routes.push_back(route); + } + return routes; +} + +static std::uint32_t HostInterfaceMtu(const char* name) { + const int descriptor = socket(AF_INET, SOCK_DGRAM, 0); + if (descriptor < 0) throw std::runtime_error("sceNetCtlGetInfo: no socket to read the host interface MTU"); + ifreq request{}; + std::strncpy(request.ifr_name, name, IFNAMSIZ - 1); + const int result = ioctl(descriptor, SIOCGIFMTU, &request); + close(descriptor); + if (result != 0) throw std::runtime_error(std::string("sceNetCtlGetInfo: no MTU for host interface ") + name); + return static_cast<std::uint32_t>(request.ifr_mtu); +} + +static std::vector<in_addr> HostDnsServers() { + std::ifstream config("/etc/resolv.conf"); + std::vector<in_addr> servers; + std::string line; + while (std::getline(config, line)) { + std::istringstream fields(line); + std::string keyword; + std::string value; + in_addr server{}; + if (fields >> keyword >> value && keyword == "nameserver" && inet_pton(AF_INET, value.c_str(), &server) == 1) servers.push_back(server); + } + return servers; +} +#endif + +static bool FindHostIpv4Interface(HostIpv4Interface& found) { +#ifdef _WIN32 + ULONG size = 0; + const ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_INCLUDE_GATEWAYS; + if (GetAdaptersAddresses(AF_INET, flags, nullptr, nullptr, &size) != ERROR_BUFFER_OVERFLOW) return false; + std::vector<std::uint8_t> storage(size); + auto* adapters = reinterpret_cast<IP_ADAPTER_ADDRESSES*>(storage.data()); + if (GetAdaptersAddresses(AF_INET, flags, nullptr, adapters, &size) != NO_ERROR) return false; + bool chosen = false; + bool chosenHasGateway = false; + ULONG chosenMetric = 0; + for (auto* adapter = adapters; adapter; adapter = adapter->Next) { + if (adapter->OperStatus != IfOperStatusUp || adapter->IfType == IF_TYPE_SOFTWARE_LOOPBACK) continue; + auto* unicast = adapter->FirstUnicastAddress; + while (unicast && !(Ipv4Of(unicast->Address) && UsableIpv4(Ipv4Of(unicast->Address)->sin_addr))) unicast = unicast->Next; + if (!unicast) continue; + auto* gateway = adapter->FirstGatewayAddress; + while (gateway && !Ipv4Of(gateway->Address)) gateway = gateway->Next; + if (chosen && !(gateway && (!chosenHasGateway || adapter->Ipv4Metric < chosenMetric))) continue; + HostIpv4Interface candidate; + candidate.address = Ipv4Of(unicast->Address)->sin_addr; + candidate.netmask.s_addr = PrefixMask(unicast->OnLinkPrefixLength); + if (gateway) candidate.gateway = Ipv4Of(gateway->Address)->sin_addr; + candidate.mtu = adapter->Mtu; + for (auto* server = adapter->FirstDnsServerAddress; server; server = server->Next) { + if (Ipv4Of(server->Address)) candidate.dns.push_back(Ipv4Of(server->Address)->sin_addr); + } + found = std::move(candidate); + chosen = true; + chosenHasGateway = gateway != nullptr; + chosenMetric = adapter->Ipv4Metric; + } + return chosen; +#else + ifaddrs* list = nullptr; + if (getifaddrs(&list) != 0) return false; + const std::unique_ptr<ifaddrs, void (*)(ifaddrs*)> interfaces(list, freeifaddrs); + const auto routes = HostDefaultRoutes(); + const ifaddrs* chosen = nullptr; + const HostDefaultRoute* chosenRoute = nullptr; + for (auto* item = list; item; item = item->ifa_next) { + if (!item->ifa_addr || item->ifa_addr->sa_family != AF_INET || !(item->ifa_flags & IFF_UP) || (item->ifa_flags & IFF_LOOPBACK)) continue; + if (!UsableIpv4(reinterpret_cast<const sockaddr_in*>(item->ifa_addr)->sin_addr)) continue; + const HostDefaultRoute* route = nullptr; + for (const auto& entry : routes) { + if (entry.name == item->ifa_name && (!route || entry.metric < route->metric)) route = &entry; + } + if (chosen && !(route && (!chosenRoute || route->metric < chosenRoute->metric))) continue; + chosen = item; + chosenRoute = route; + } + if (!chosen) return false; + found.address = reinterpret_cast<const sockaddr_in*>(chosen->ifa_addr)->sin_addr; + if (chosen->ifa_netmask) found.netmask = reinterpret_cast<const sockaddr_in*>(chosen->ifa_netmask)->sin_addr; + if (chosenRoute) found.gateway = chosenRoute->gateway; + found.mtu = HostInterfaceMtu(chosen->ifa_name); + found.dns = HostDnsServers(); + return true; +#endif +} + +static void WriteInfoWord(NetCtlInfo* info, std::uint32_t value) { + std::memcpy(info->opaque, &value, sizeof(value)); +} + +static void WriteInfoAddress(NetCtlInfo* info, in_addr address) { + const auto* bytes = reinterpret_cast<const std::uint8_t*>(&address.s_addr); + char text[NET_CTL_IPV4_ADDR_STR_LEN] = {}; + std::snprintf(text, sizeof(text), "%u.%u.%u.%u", static_cast<unsigned>(bytes[0]), static_cast<unsigned>(bytes[1]), static_cast<unsigned>(bytes[2]), static_cast<unsigned>(bytes[3])); + std::memcpy(info->opaque, text, sizeof(text)); +} + extern "C" std::uint64_t NetCtlReadByteCounter_nid_no_patch(const char* path) { std::ifstream file(path); std::uint64_t value = 0; @@ -135,9 +312,25 @@ int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat* stat) { } int APS5_VABI sceNetCtlGetInfo(int code, NetCtlInfo* info) { - (void)code; - (void)info; - return SCE_NET_CTL_ERROR_NOT_CONNECTED; + if (!info) return SCE_NET_CTL_ERROR_INVALID_ADDR; + if (code < NET_CTL_INFO_DEVICE || code > NET_CTL_INFO_HTTP_PROXY_PORT) return SCE_NET_CTL_ERROR_INVALID_CODE; + HostIpv4Interface host; + if (!FindHostIpv4Interface(host)) return SCE_NET_CTL_ERROR_NOT_CONNECTED; + switch (code) { + case NET_CTL_INFO_DEVICE: WriteInfoWord(info, NET_CTL_DEVICE_WIRED); break; + case NET_CTL_INFO_ETHER_ADDR: std::memcpy(info->opaque, NET_CTL_ETHER_ADDR, sizeof(NET_CTL_ETHER_ADDR)); break; + case NET_CTL_INFO_MTU: WriteInfoWord(info, host.mtu); break; + case NET_CTL_INFO_LINK: WriteInfoWord(info, NET_CTL_LINK_CONNECTED); break; + case NET_CTL_INFO_IP_CONFIG: WriteInfoWord(info, NET_CTL_IP_DHCP); break; + case NET_CTL_INFO_IP_ADDRESS: WriteInfoAddress(info, host.address); break; + case NET_CTL_INFO_NETMASK: WriteInfoAddress(info, host.netmask); break; + case NET_CTL_INFO_DEFAULT_ROUTE: WriteInfoAddress(info, host.gateway); break; + case NET_CTL_INFO_PRIMARY_DNS: WriteInfoAddress(info, host.dns.size() > 0 ? host.dns[0] : in_addr{}); break; + case NET_CTL_INFO_SECONDARY_DNS: WriteInfoAddress(info, host.dns.size() > 1 ? host.dns[1] : in_addr{}); break; + case NET_CTL_INFO_HTTP_PROXY_CONFIG: WriteInfoWord(info, NET_CTL_HTTP_PROXY_OFF); break; + default: throw std::runtime_error("sceNetCtlGetInfo: info code " + std::to_string(code) + " is not implemented"); + } + return 0; } int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo* nat_info) { diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp new file mode 100644 index 00000000000..f0196de5a72 --- /dev/null +++ b/core/libs/tests/GuestNetCtl.cpp @@ -0,0 +1,136 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include <winsock2.h> +#include <ws2tcpip.h> +#else +#include <arpa/inet.h> +#include <netinet/in.h> +#include <sys/socket.h> +#include <unistd.h> +#endif +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include <cstddef> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <initializer_list> +#include <stdexcept> + +extern "C" { +int APS5_VABI sceNetCtlGetInfo(int, NetCtlInfo*); +int APS5_VABI sceNetCtlGetState(int*); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kErrInvalidCode = static_cast<int>(0x80412106); +constexpr int kErrInvalidAddr = static_cast<int>(0x80412107); +constexpr int kErrNotConnected = static_cast<int>(0x80412108); +constexpr int kStateIpObtained = 3; +constexpr std::uint8_t kFill = 0xa5; +constexpr std::size_t kAddressLength = 16; + +NetCtlInfo Query(int code) { + NetCtlInfo info; + std::memset(&info, kFill, sizeof(info)); + Require(sceNetCtlGetInfo(code, &info) == 0); + return info; +} + +void RequireUntouchedFrom(const NetCtlInfo& info, std::size_t offset) { + for (std::size_t i = offset; i < sizeof(info.opaque); ++i) Require(info.opaque[i] == kFill); +} + +std::uint32_t QueryWord(int code) { + const auto info = Query(code); + std::uint32_t value = 0; + std::memcpy(&value, info.opaque, sizeof(value)); + RequireUntouchedFrom(info, sizeof(value)); + return value; +} + +in_addr QueryAddress(int code) { + const auto info = Query(code); + char text[kAddressLength]; + std::memcpy(text, info.opaque, sizeof(text)); + Require(std::memchr(text, '\0', sizeof(text)) != nullptr); + RequireUntouchedFrom(info, kAddressLength); + in_addr address{}; + Require(inet_pton(AF_INET, text, &address) == 1); + return address; +} + +bool Binds(in_addr address) { + sockaddr_in local{}; + local.sin_family = AF_INET; + local.sin_addr = address; +#ifdef _WIN32 + const SOCKET descriptor = socket(AF_INET, SOCK_DGRAM, 0); + Require(descriptor != INVALID_SOCKET); + const bool bound = bind(descriptor, reinterpret_cast<const sockaddr*>(&local), sizeof(local)) == 0; + closesocket(descriptor); +#else + const int descriptor = socket(AF_INET, SOCK_DGRAM, 0); + Require(descriptor >= 0); + const bool bound = bind(descriptor, reinterpret_cast<const sockaddr*>(&local), sizeof(local)) == 0; + close(descriptor); +#endif + return bound; +} + +} + +int main() { +#ifdef _WIN32 + WSADATA data{}; + Require(WSAStartup(MAKEWORD(2, 2), &data) == 0); +#endif + NetCtlInfo info{}; + Require(sceNetCtlGetInfo(0, &info) == kErrInvalidCode); + Require(sceNetCtlGetInfo(22, &info) == kErrInvalidCode); + Require(sceNetCtlGetInfo(14, nullptr) == kErrInvalidAddr); + + const int result = sceNetCtlGetInfo(14, &info); + if (result == kErrNotConnected) { + std::puts("skipped, the host has no IPv4 address"); + return 77; + } + Require(result == 0); + int state = -1; + Require(sceNetCtlGetState(&state) == 0 && state == kStateIpObtained); + + Require(QueryWord(1) == 0); + const auto ether = Query(2); + const std::uint8_t mac[6] = {0x02, 0x50, 0x53, 0x35, 0x00, 0x01}; + Require(std::memcmp(ether.opaque, mac, sizeof(mac)) == 0); + RequireUntouchedFrom(ether, sizeof(mac)); + const auto mtu = QueryWord(3); + Require(mtu >= 68 && mtu <= 65535); + Require(QueryWord(4) == 1); + Require(QueryWord(11) == 0); + Require(QueryWord(19) == 0); + + const auto address = QueryAddress(14); + const auto host = ntohl(address.s_addr); + Require(host != 0 && (host >> 24) != 127); + Require(Binds(address)); + const auto mask = ntohl(QueryAddress(15).s_addr); + Require(mask != 0 && (~mask & (~mask + 1)) == 0); + QueryAddress(16); + QueryAddress(17); + QueryAddress(18); + + for (const int code : {5, 6, 7, 8, 9, 10, 12, 13, 20, 21}) { + bool threw = false; + try { + sceNetCtlGetInfo(code, &info); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + } +} diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md new file mode 100644 index 00000000000..4da9e097aac --- /dev/null +++ b/docs/dev/debt/libSceNetCtl.md @@ -0,0 +1,5 @@ +# libSceNetCtl technical debt + +### Unknown function info + +- [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host interface that holds an IPv4 address, preferring the one that carries the default route: its address, netmask, gateway, MTU and first two IPv4 DNS servers. The device is reported as wired, the link as connected, the IP configuration as DHCP and the HTTP proxy as off, whatever the host uses, and the Ethernet address is the one `sceNetGetMacAddress` reports. A gateway or DNS server the interface does not have is reported as `0.0.0.0`; what a console reports then is unknown. The Wi-Fi codes (5-10), the DHCP host name (12), the PPPoE user name (13) and the HTTP proxy server and port (20, 21) throw From 95e51985d4a032a9f9cf8002af9588a514691b7d Mon Sep 17 00:00:00 2001 From: Rick <99503318+0xRickx@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:02:00 +0200 Subject: [PATCH 16/81] fix(libSceJpegDec): use the JpegDec parameter and image info layouts The JpegDec structures were libScePngDec's under new names. OpenOrbis jpeg.h and SharpProspero JpegDec.cs agree on a 12-byte create param, a 0x30-byte decode param with a coefficient buffer, decode mode and down scale, and a 0x24-byte image info that reports the coefficient memory size, the suitable CSC attribute and the output size. Accept the CSC attributes 1 and 2 that SharpProspero passes, fill the output size, component count, coefficient memory size and suitable attribute, apply the requested alpha, and throw for decode modes other than 0 and down scales other than 1. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --- core/libs/SceTypes.hpp | 21 +++- core/libs/prx/libSceJpegDec/Export.cpp | 34 +++++-- core/libs/tests/GuestJpegDec.cpp | 133 ++++++++++++++++++------- docs/dev/debt/libSceJpegDec.md | 9 ++ 4 files changed, 148 insertions(+), 49 deletions(-) create mode 100644 docs/dev/debt/libSceJpegDec.md diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 3f732329498..3b56c098652 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1750,23 +1750,29 @@ struct JpegEncOutputInfo { }; struct JpegDecCreateParam { - std::uint32_t size; + std::uint32_t this_size; std::uint32_t attribute; + std::uint32_t max_image_width; }; struct JpegDecParseParam { const void* jpeg_mem_addr; std::uint32_t jpeg_mem_size; - std::uint32_t reserved0; + std::uint16_t decode_mode; + std::uint16_t down_scale; }; struct JpegDecDecodeParam { const void* jpeg_mem_addr; void* image_mem_addr; + void* coefficient_mem_addr; std::uint32_t jpeg_mem_size; std::uint32_t image_mem_size; + std::uint32_t coefficient_mem_size; + std::uint16_t decode_mode; + std::uint16_t down_scale; std::uint16_t pixel_format; - std::uint16_t reserved0; + std::uint16_t alpha_value; std::uint32_t image_pitch; }; @@ -1774,8 +1780,13 @@ struct JpegDecImageInfo { std::uint32_t image_width; std::uint32_t image_height; std::uint16_t color_space; - std::uint16_t bit_depth; - std::uint32_t image_flag; + std::uint16_t component_number; + std::uint8_t component_id[4]; + std::uint8_t sampling_factor[4]; + std::uint32_t coefficient_mem_size; + std::uint32_t suitable_csc_attribute; + std::uint32_t output_image_width; + std::uint32_t output_image_height; }; struct PlayGoInitParams { diff --git a/core/libs/prx/libSceJpegDec/Export.cpp b/core/libs/prx/libSceJpegDec/Export.cpp index d38764bf71f..0d92c485bb0 100644 --- a/core/libs/prx/libSceJpegDec/Export.cpp +++ b/core/libs/prx/libSceJpegDec/Export.cpp @@ -5,14 +5,15 @@ #include <optional> #include <span> #include <stdexcept> +#include <string> #include <utility> #include "Decoder/Jpeg.hpp" #include "SceTypes.hpp" -static_assert(sizeof(JpegDecCreateParam) == 0x8); +static_assert(sizeof(JpegDecCreateParam) == 0xC); static_assert(sizeof(JpegDecParseParam) == 0x10); -static_assert(sizeof(JpegDecDecodeParam) == 0x20); -static_assert(sizeof(JpegDecImageInfo) == 0x10); +static_assert(sizeof(JpegDecDecodeParam) == 0x30); +static_assert(sizeof(JpegDecImageInfo) == 0x24); namespace { @@ -24,10 +25,14 @@ constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_WORK_MEMORY = static_cast<std: constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_DATA = static_cast<std::int32_t>(0x80690010); constexpr std::int32_t SCE_JPEG_DEC_ERROR_DECODE_ERROR = static_cast<std::int32_t>(0x80690012); -constexpr std::uint32_t ATTRIBUTE_NONE = 0; +constexpr std::uint32_t ATTRIBUTE_CSC_ORDINARY = 1; +constexpr std::uint32_t ATTRIBUTE_CSC_FULL = 2; constexpr std::uint32_t MEMORY_SIZE = 0x20; constexpr std::uintptr_t HANDLE_ALIGNMENT = 8; +constexpr std::uint16_t DECODE_MODE_NORMAL = 0; +constexpr std::uint16_t DOWN_SCALE_NONE = 1; + constexpr std::uint16_t PIXEL_FORMAT_R8G8B8A8 = 0; constexpr std::uint16_t PIXEL_FORMAT_B8G8R8A8 = 1; @@ -45,11 +50,16 @@ static_assert(sizeof(Context) + HANDLE_ALIGNMENT - 1 <= MEMORY_SIZE); std::int32_t validateCreateParam(const JpegDecCreateParam* param) { if (!param) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; - if (param->size != sizeof(JpegDecCreateParam)) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; - if (param->attribute != ATTRIBUTE_NONE) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + if (param->this_size != sizeof(JpegDecCreateParam)) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; + if (param->attribute != ATTRIBUTE_CSC_ORDINARY && param->attribute != ATTRIBUTE_CSC_FULL) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; return 0; } +void requireNormalDecode(const char* function, std::uint16_t decodeMode, std::uint16_t downScale) { + if (decodeMode != DECODE_MODE_NORMAL) throw std::runtime_error(std::string(function) + ": decode mode " + std::to_string(decodeMode) + " is not implemented"); + if (downScale != DOWN_SCALE_NONE) throw std::runtime_error(std::string(function) + ": down scale " + std::to_string(downScale) + " is not implemented"); +} + Context* toContext(void* handle) { const auto address = reinterpret_cast<std::uintptr_t>(handle); if (address == 0 || address % HANDLE_ALIGNMENT != 0) return nullptr; @@ -58,11 +68,14 @@ Context* toContext(void* handle) { } void fillImageInfo(const Decoder::Jpeg::Header& header, JpegDecImageInfo* info) { + *info = {}; info->image_width = header.width; info->image_height = header.height; info->color_space = COLOR_SPACE_RGB; - info->bit_depth = 8; - info->image_flag = 0; + info->component_number = static_cast<std::uint16_t>(header.channels); + info->suitable_csc_attribute = ATTRIBUTE_CSC_ORDINARY; + info->output_image_width = header.width; + info->output_image_height = header.height; } std::span<const std::uint8_t> toBytes(const void* data, std::uint32_t size) { @@ -95,6 +108,7 @@ int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param if (!param->jpeg_mem_addr || !param->image_mem_addr) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; if (param->jpeg_mem_size == 0 || param->image_mem_size == 0) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; if (param->pixel_format != PIXEL_FORMAT_R8G8B8A8 && param->pixel_format != PIXEL_FORMAT_B8G8R8A8) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + requireNormalDecode(__func__, param->decode_mode, param->down_scale); const std::span<const std::uint8_t> jpeg = toBytes(param->jpeg_mem_addr, param->jpeg_mem_size); const std::optional<Decoder::Jpeg::Header> header = Decoder::Jpeg::ParseHeader(jpeg); @@ -109,6 +123,7 @@ int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param if (!image || image->width != header->width || image->height != header->height) return SCE_JPEG_DEC_ERROR_DECODE_ERROR; const bool swapRedBlue = param->pixel_format == PIXEL_FORMAT_B8G8R8A8; + const auto alpha = static_cast<std::uint8_t>(param->alpha_value); auto* output = static_cast<std::uint8_t*>(param->image_mem_addr); for (std::uint32_t y = 0; y < image->height; ++y) { std::uint8_t* out = output + y * pitch; @@ -125,7 +140,7 @@ int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param pixel[2] = in[2]; } if (swapRedBlue) std::swap(pixel[0], pixel[2]); - pixel[3] = 0xFF; + pixel[3] = alpha; } } @@ -145,6 +160,7 @@ int32_t APS5_VABI sceJpegDecParseHeader(const JpegDecParseParam* param, JpegDecI if (!param) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; if (!param->jpeg_mem_addr || !image_info) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; if (param->jpeg_mem_size == 0) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; + requireNormalDecode(__func__, param->decode_mode, param->down_scale); const std::optional<Decoder::Jpeg::Header> header = Decoder::Jpeg::ParseHeader(toBytes(param->jpeg_mem_addr, param->jpeg_mem_size)); if (!header) return SCE_JPEG_DEC_ERROR_INVALID_DATA; fillImageInfo(*header, image_info); diff --git a/core/libs/tests/GuestJpegDec.cpp b/core/libs/tests/GuestJpegDec.cpp index 1268bd57d22..0d9e9dcf430 100644 --- a/core/libs/tests/GuestJpegDec.cpp +++ b/core/libs/tests/GuestJpegDec.cpp @@ -1,12 +1,28 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "Decoder/Jpeg.hpp" +#include <cstddef> #include <cstdint> #include <cstdlib> #include <cstring> +#include <optional> #include <stdexcept> #include <vector> +static_assert(sizeof(JpegDecCreateParam) == 0xC); +static_assert(sizeof(JpegDecParseParam) == 0x10); +static_assert(offsetof(JpegDecParseParam, jpeg_mem_size) == 0x8 && offsetof(JpegDecParseParam, decode_mode) == 0xC && offsetof(JpegDecParseParam, down_scale) == 0xE); +static_assert(sizeof(JpegDecDecodeParam) == 0x30); +static_assert(offsetof(JpegDecDecodeParam, coefficient_mem_addr) == 0x10 && offsetof(JpegDecDecodeParam, jpeg_mem_size) == 0x18); +static_assert(offsetof(JpegDecDecodeParam, image_mem_size) == 0x1C && offsetof(JpegDecDecodeParam, coefficient_mem_size) == 0x20); +static_assert(offsetof(JpegDecDecodeParam, decode_mode) == 0x24 && offsetof(JpegDecDecodeParam, down_scale) == 0x26); +static_assert(offsetof(JpegDecDecodeParam, pixel_format) == 0x28 && offsetof(JpegDecDecodeParam, alpha_value) == 0x2A && offsetof(JpegDecDecodeParam, image_pitch) == 0x2C); +static_assert(sizeof(JpegDecImageInfo) == 0x24); +static_assert(offsetof(JpegDecImageInfo, color_space) == 0x8 && offsetof(JpegDecImageInfo, component_number) == 0xA); +static_assert(offsetof(JpegDecImageInfo, component_id) == 0xC && offsetof(JpegDecImageInfo, sampling_factor) == 0x10); +static_assert(offsetof(JpegDecImageInfo, coefficient_mem_size) == 0x14 && offsetof(JpegDecImageInfo, suitable_csc_attribute) == 0x18); +static_assert(offsetof(JpegDecImageInfo, output_image_width) == 0x1C && offsetof(JpegDecImageInfo, output_image_height) == 0x20); + extern "C" { std::int32_t APS5_VABI sceJpegDecQueryMemorySize(const JpegDecCreateParam*); std::int32_t APS5_VABI sceJpegDecCreate(const JpegDecCreateParam*, void*, std::uint32_t, void**); @@ -17,21 +33,35 @@ std::int32_t APS5_VABI sceJpegDecDecode(void*, const JpegDecDecodeParam*, JpegDe static void Require(bool value) { if (!value) std::abort(); } -static std::vector<std::uint8_t> TestJpeg(std::uint32_t width, std::uint32_t height) { - std::vector<std::uint8_t> rgb(static_cast<std::size_t>(width) * height * 3); - for (std::size_t i = 0; i < rgb.size(); ++i) rgb[i] = static_cast<std::uint8_t>(10 + i * 13); - return Decoder::Jpeg::Encode(rgb, width, height, 3, 90); +template<typename TFunction> +static bool ThrowsRuntimeError(TFunction function) { + try { + function(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +static std::vector<std::uint8_t> TestJpeg(std::uint32_t width, std::uint32_t height, std::uint32_t channels) { + std::vector<std::uint8_t> pixels(static_cast<std::size_t>(width) * height * channels); + for (std::size_t i = 0; i < pixels.size(); ++i) pixels[i] = static_cast<std::uint8_t>(10 + i * 13); + return Decoder::Jpeg::Encode(pixels, width, height, channels, 90); } -static JpegDecDecodeParam DecodeParam(const std::vector<std::uint8_t>& jpeg, std::vector<std::uint8_t>& image) { +static JpegDecDecodeParam DecodeParam(const std::vector<std::uint8_t>& jpeg, std::vector<std::uint8_t>& image, std::uint32_t pitch) { JpegDecDecodeParam param{}; param.jpeg_mem_addr = jpeg.data(); param.image_mem_addr = image.data(); + param.coefficient_mem_addr = nullptr; param.jpeg_mem_size = static_cast<std::uint32_t>(jpeg.size()); param.image_mem_size = static_cast<std::uint32_t>(image.size()); + param.coefficient_mem_size = 0; + param.decode_mode = 0; + param.down_scale = 1; param.pixel_format = 0; - param.reserved0 = 0; - param.image_pitch = 0; + param.alpha_value = 0xFF; + param.image_pitch = pitch; return param; } @@ -43,62 +73,95 @@ int main() { constexpr std::int32_t invalidWorkMemory = static_cast<std::int32_t>(0x80690005); constexpr std::int32_t invalidData = static_cast<std::int32_t>(0x80690010); - const JpegDecCreateParam param{sizeof(JpegDecCreateParam), 0}; + const std::vector<std::uint8_t> jpeg = TestJpeg(7, 5, 3); + JpegDecImageInfo info{}; + const JpegDecParseParam parseParam{jpeg.data(), static_cast<std::uint32_t>(jpeg.size()), 0, 1}; + Require(sceJpegDecParseHeader(nullptr, &info) == invalidParam); + Require(sceJpegDecParseHeader(&parseParam, nullptr) == invalidAddr); + const JpegDecParseParam emptyParam{nullptr, 0, 0, 1}; + Require(sceJpegDecParseHeader(&emptyParam, &info) == invalidAddr); + const JpegDecParseParam downScaledParse{jpeg.data(), static_cast<std::uint32_t>(jpeg.size()), 0, 2}; + Require(ThrowsRuntimeError([&] { sceJpegDecParseHeader(&downScaledParse, &info); })); + const std::vector<std::uint8_t> grayJpeg = TestJpeg(3, 2, 1); + const JpegDecParseParam grayParse{grayJpeg.data(), static_cast<std::uint32_t>(grayJpeg.size()), 0, 1}; + Require(sceJpegDecParseHeader(&grayParse, &info) == 0); + Require(info.image_width == 3 && info.image_height == 2 && info.component_number == 1); + Require(sceJpegDecParseHeader(&parseParam, &info) == 0); + Require(info.image_width == 7 && info.image_height == 5 && info.component_number == 3); + Require(info.output_image_width == 7 && info.output_image_height == 5); + Require(info.coefficient_mem_size == 0 && info.suitable_csc_attribute == 1); + + const JpegDecCreateParam param{sizeof(JpegDecCreateParam), info.suitable_csc_attribute, info.image_width}; const std::int32_t memorySize = sceJpegDecQueryMemorySize(¶m); Require(memorySize > 0); + const JpegDecCreateParam fullParam{sizeof(JpegDecCreateParam), 2, info.image_width}; + Require(sceJpegDecQueryMemorySize(&fullParam) == memorySize); Require(sceJpegDecQueryMemorySize(nullptr) == invalidParam); - const JpegDecCreateParam badSize{sizeof(JpegDecCreateParam) - 1, 0}; + const JpegDecCreateParam badSize{sizeof(JpegDecCreateParam) - 4, 1, 7}; Require(sceJpegDecQueryMemorySize(&badSize) == invalidSize); - const JpegDecCreateParam badAttribute{sizeof(JpegDecCreateParam), 1}; - Require(sceJpegDecQueryMemorySize(&badAttribute) == invalidParam); + for (const std::uint32_t attribute : {0u, 3u}) { + const JpegDecCreateParam badAttribute{sizeof(JpegDecCreateParam), attribute, 7}; + Require(sceJpegDecQueryMemorySize(&badAttribute) == invalidParam); + } std::vector<std::uint8_t> memory(static_cast<std::size_t>(memorySize) + 8); void* handle = nullptr; Require(sceJpegDecCreate(¶m, memory.data(), 0, &handle) == invalidWorkMemory); Require(sceJpegDecCreate(¶m, memory.data(), static_cast<std::uint32_t>(memorySize), nullptr) == invalidAddr); - Require(sceJpegDecCreate(¶m, memory.data(), static_cast<std::uint32_t>(memorySize), &handle) == 0); + Require(sceJpegDecCreate(&fullParam, memory.data(), static_cast<std::uint32_t>(memorySize), &handle) == 0); Require(handle != nullptr); Require(sceJpegDecDelete(nullptr) == invalidHandle); Require(sceJpegDecDelete(handle) == 0); Require(sceJpegDecCreate(¶m, memory.data(), static_cast<std::uint32_t>(memorySize), &handle) == 0); - const std::vector<std::uint8_t> jpeg = TestJpeg(7, 5); - JpegDecImageInfo info{}; - const JpegDecParseParam parseParam{jpeg.data(), static_cast<std::uint32_t>(jpeg.size()), 0}; - Require(sceJpegDecParseHeader(nullptr, &info) == invalidParam); - Require(sceJpegDecParseHeader(&parseParam, nullptr) == invalidAddr); - const JpegDecParseParam emptyParam{nullptr, 0, 0}; - Require(sceJpegDecParseHeader(&emptyParam, &info) == invalidAddr); - Require(sceJpegDecParseHeader(&parseParam, &info) == 0); - Require(info.image_width == 7); - Require(info.image_height == 5); + const std::optional<Decoder::Jpeg::Image> reference = Decoder::Jpeg::Decode(jpeg); + Require(reference && reference->width == 7 && reference->height == 5 && reference->channels == 3); std::vector<std::uint8_t> image(7 * 5 * 4, 0xCC); - JpegDecDecodeParam decodeParam = DecodeParam(jpeg, image); + JpegDecDecodeParam decodeParam = DecodeParam(jpeg, image, 7 * 4); Require(sceJpegDecDecode(nullptr, &decodeParam, &info) == invalidHandle); Require(sceJpegDecDecode(handle, nullptr, &info) == invalidParam); JpegDecDecodeParam badFormat = decodeParam; badFormat.pixel_format = 2; Require(sceJpegDecDecode(handle, &badFormat, &info) == invalidParam); - const std::int32_t packed = sceJpegDecDecode(handle, &decodeParam, &info); - Require(packed == static_cast<std::int32_t>(7 << 16 | 5)); - Require(info.image_width == 7); - Require(info.image_height == 5); - Require(image[3] == 0xFF); - - std::vector<std::uint8_t> bgra(7 * 5 * 4, 0); - JpegDecDecodeParam bgraParam = DecodeParam(jpeg, bgra); + JpegDecDecodeParam downScaled = decodeParam; + downScaled.down_scale = 2; + Require(ThrowsRuntimeError([&] { sceJpegDecDecode(handle, &downScaled, &info); })); + JpegDecDecodeParam otherMode = decodeParam; + otherMode.decode_mode = 1; + Require(ThrowsRuntimeError([&] { sceJpegDecDecode(handle, &otherMode, &info); })); + info = {}; + Require(sceJpegDecDecode(handle, &decodeParam, &info) == static_cast<std::int32_t>(7 << 16 | 5)); + Require(info.image_width == 7 && info.image_height == 5); + Require(info.output_image_width == 7 && info.output_image_height == 5); + for (std::size_t pixel = 0; pixel < 7 * 5; ++pixel) { + for (std::size_t c = 0; c < 3; ++c) Require(image[pixel * 4 + c] == reference->pixels[pixel * 3 + c]); + Require(image[pixel * 4 + 3] == 0xFF); + } + + constexpr std::uint32_t pitch = 7 * 4 + 8; + std::vector<std::uint8_t> bgra(4 * pitch + 7 * 4, 0xEE); + JpegDecDecodeParam bgraParam = DecodeParam(jpeg, bgra, pitch); bgraParam.pixel_format = 1; + bgraParam.alpha_value = 0x77; Require(sceJpegDecDecode(handle, &bgraParam, nullptr) == static_cast<std::int32_t>(7 << 16 | 5)); - Require(bgra[0] == image[2]); - Require(bgra[2] == image[0]); + for (std::size_t y = 0; y < 5; ++y) { + for (std::size_t x = 0; x < 7; ++x) { + const std::uint8_t* pixel = &bgra[y * pitch + x * 4]; + const std::uint8_t* rgba = &image[(y * 7 + x) * 4]; + Require(pixel[0] == rgba[2] && pixel[1] == rgba[1] && pixel[2] == rgba[0] && pixel[3] == 0x77); + } + } + for (std::size_t y = 0; y < 4; ++y) { + for (std::size_t x = 7 * 4; x < pitch; ++x) Require(bgra[y * pitch + x] == 0xEE); + } const std::vector<std::uint8_t> garbage{0x00, 0x01, 0x02, 0x03}; - JpegDecParseParam garbageParam{garbage.data(), static_cast<std::uint32_t>(garbage.size()), 0}; + const JpegDecParseParam garbageParam{garbage.data(), static_cast<std::uint32_t>(garbage.size()), 0, 1}; Require(sceJpegDecParseHeader(&garbageParam, &info) == invalidData); std::vector<std::uint8_t> garbageOut(64, 0); - JpegDecDecodeParam garbageDecode = DecodeParam(garbage, garbageOut); + const JpegDecDecodeParam garbageDecode = DecodeParam(garbage, garbageOut, 4 * 4); Require(sceJpegDecDecode(handle, &garbageDecode, &info) == invalidData); Require(sceJpegDecDelete(handle) == 0); diff --git a/docs/dev/debt/libSceJpegDec.md b/docs/dev/debt/libSceJpegDec.md new file mode 100644 index 00000000000..342b2df1719 --- /dev/null +++ b/docs/dev/debt/libSceJpegDec.md @@ -0,0 +1,9 @@ +# libSceJpegDec technical debt + +### Unknown function info + +- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - structure layouts follow [OpenOrbis `jpeg.h`](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/_types/jpeg.h#L13-L72) and [SharpProspero `JpegDec.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Image/JpegDec.cs#L20-L117); attributes 1 and 2 are the values [SharpProspero's `JpegImage.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Graphics/JpegImage.cs#L64-L74) passes. The error codes are libScePngDec's: no reference lists the JpegDec ones, and SharpProspero raises 0x80650020 on its JpegDec path where it raises libScePngDec's 0x80690020 on its PngDec path. Image info always reports color space 3, suitable CSC attribute 1 and zero component ids and sampling factors; the console's values, including which images it reports attribute 2 for, are unknown. The coefficient memory size is 0 because the host decoder needs no coefficient buffer; the size the console reports for progressive images is unknown. `max_image_width` is not checked, and the packed `width << 16 | height` decode result is libScePngDec's + +### Functional + +- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - decode modes other than 0 and down scales other than 1 throw From 312489589fcd63faa5a79a7231cdbb17fe5f0c00 Mon Sep 17 00:00:00 2001 From: houcineBen <77234345+houcineBen@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:42:50 +0200 Subject: [PATCH 17/81] fix(shader): put the compute wave index in TG_SIZE[11:6] GCN and RDNA code reads a compute wave's index in its workgroup from TG_SIZE[11:6] (ACO: s_bfe_u32 tg_size, 6 | 6 << 16). The prologue wrote it only at bit 20, so every wave read 0 there and cross-wave reductions such as RADV's radix sort lost the carries of all but wave 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../tests/execution/TgSizeWaveId.cpp | 94 +++++++++++++++++++ .../Translation/src/InstructionTranslator.cpp | 2 +- 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/TgSizeWaveId.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/TgSizeWaveId.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/TgSizeWaveId.cpp new file mode 100644 index 00000000000..79d60e331fb --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/TgSizeWaveId.cpp @@ -0,0 +1,94 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include <windows.h> +#endif +#include <array> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <iostream> +#include <string> +#include <vector> + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Threads = 256; + +alignas(256) constexpr std::array<std::uint32_t, 8> TgSizeWaveIdCode{ + 0x9384ff03, 0x00060006, 0x34020082, 0x7e040204, 0xdc708000, 0x00000201, 0xbf810000, 0xbf810000, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "TG_SIZE wave id: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + ~GuestBlock() { GuestAllocations::Mutation().Remove(block); } + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +void Run(AgcDriver::VulkanDevice& device, GuestBlock& guest, std::uint32_t waveSize) { + std::memset(guest.Data(), 0xff, BlockBytes); + const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(guest.Data())); + const std::vector<std::uint32_t> userData{static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u)}; + const std::span<const std::uint32_t> code(TgSizeWaveIdCode); + const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {true, false, false}, true, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data())); + device.WaitIdle(); + std::array<std::uint32_t, Threads> waves{}; + std::memcpy(waves.data(), guest.Data(), sizeof(waves)); + for (std::uint32_t thread = 0; thread < Threads; ++thread) { + if (waves[thread] == thread / waveSize) continue; + Require(false, "wave" + std::to_string(waveSize) + " thread " + std::to_string(thread) + " read wave " + std::to_string(waves[thread]) + " from TG_SIZE[11:6], expected " + std::to_string(thread / waveSize)); + } + std::printf("wave%u: TG_SIZE[11:6] holds the wave index for all %u threads\n", waveSize, Threads); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock guest; + Run(*device, guest, 64); + Run(*device, guest, 32); + std::puts("TG_SIZE wave id tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index 247197f835a..9f8e79b7dee 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -231,7 +231,7 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO const std::uint32_t waves = std::min((totalThreads + waveSize - 1u) / waveSize, 0x3fu); IrValue& localIndex = builtin(StageInputKind::LocalInvocationIndex); IrValue& waveId = entryIr.Emit(IrOpcode::UDiv32, IrOpcodeType(IrOpcode::UDiv32), {&localIndex, &entryIr.Constant(waveSize)}); - IrValue& waveBits = entryIr.ShiftLeftLogical(waveId, entryIr.Constant(20u)); + IrValue& waveBits = entryIr.BitwiseOr(entryIr.ShiftLeftLogical(waveId, entryIr.Constant(6u)), entryIr.ShiftLeftLogical(waveId, entryIr.Constant(20u))); IrValue& firstBit = entryIr.Select(entryIr.IEqual(waveId, entryIr.Constant(0u)), entryIr.Constant(0x80000000u), entryIr.Constant(0u)); entryIr.SetScalarReg(static_cast<ScalarReg>(cs->workgroupRegister + regOffset), entryIr.BitwiseOr(entryIr.BitwiseOr(waveBits, entryIr.Constant(waves)), firstBit)); } From 27b75311fffa9b15b222f74998c32b65d62b7c55 Mon Sep 17 00:00:00 2001 From: ee6aba54 <340958984+ee6aba54@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:10:50 +0200 Subject: [PATCH 18/81] fix(libSceFiber): end the frame chain of a fiber with a null frame Aps5FiberMain kept the fiber's entry address in rbp when it called the entry, so the guest entry saved that code address as the caller's frame. A guest walking frame pointers from inside a fiber read machine code as a frame record and faulted. libSceFiber is now built with frame pointers: Aps5FiberMain sets up a frame whose saved rbp is the zero of the initial fiber frame, and the chain ends there. --- core/libs/prx/libSceFiber/CMakeLists.txt | 1 + core/libs/tests/Fiber.cpp | 28 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/core/libs/prx/libSceFiber/CMakeLists.txt b/core/libs/prx/libSceFiber/CMakeLists.txt index d2ec7534eff..6aa8ddda2a7 100644 --- a/core/libs/prx/libSceFiber/CMakeLists.txt +++ b/core/libs/prx/libSceFiber/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(libSceFiber SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceFiber PRIVATE ${LIBS_INCLUDE_DIR}) +target_compile_options(libSceFiber PRIVATE -fno-omit-frame-pointer) configure_windows_unwind(libSceFiber) if(BUILD_TESTING) diff --git a/core/libs/tests/Fiber.cpp b/core/libs/tests/Fiber.cpp index d463848a8a8..dfa9ea64e8a 100644 --- a/core/libs/tests/Fiber.cpp +++ b/core/libs/tests/Fiber.cpp @@ -151,6 +151,25 @@ void APS5_VABI HostCallEntry(std::uint64_t, std::uint64_t) { for (;;) sceFiberReturnToThread(0, &received); } +alignas(16) std::array<unsigned char, 64 * 1024> g_chainContext; +FiberStorage g_chainStorage; +bool g_chainEnded = false; + +void APS5_VABI FrameChainEntry(std::uint64_t, std::uint64_t) { + auto* frame = static_cast<std::uintptr_t*>(__builtin_frame_address(0)); + const auto low = reinterpret_cast<std::uintptr_t>(frame); + const auto high = low + (1u << 20); + for (int depth = 0; depth < 100 && frame != nullptr; ++depth) { + const auto address = reinterpret_cast<std::uintptr_t>(frame); + Require(address >= low && address + 2 * sizeof(std::uintptr_t) <= high, "frame chain stays on the fiber's stack"); + frame = reinterpret_cast<std::uintptr_t*>(frame[0]); + Require(frame == nullptr || reinterpret_cast<std::uintptr_t>(frame) > address, "frame chain climbs the stack"); + } + g_chainEnded = frame == nullptr; + std::uint64_t received = 0; + for (;;) sceFiberReturnToThread(0, &received); +} + void RunHostCallOnSmallContext() { auto* fiber = reinterpret_cast<FiberObject*>(&g_hostCallStorage); auto* context = g_hostCallArea.data() + g_hostCallArea.size() - HostCallContextSize; @@ -163,6 +182,14 @@ void RunHostCallOnSmallContext() { } } +void RunFrameChain() { + auto* fiber = reinterpret_cast<FiberObject*>(&g_chainStorage); + Require(_sceFiberInitializeImpl_nid_postfix(fiber, "chain", FrameChainEntry, 0, g_chainContext.data(), g_chainContext.size(), nullptr, 0) == 0, "initialize chain"); + Require(sceFiberRun_nid_postfix(fiber, 0, nullptr) == 0, "run chain"); + Require(g_chainEnded, "frame chain of a fiber ends with a null frame"); + Require(sceFiberFinalize(fiber) == 0, "finalize chain"); +} + [[gnu::noinline]] std::int32_t RunFirst(std::uint64_t* returned) { g_threadFramePointer = reinterpret_cast<std::uint64_t>(__builtin_frame_address(0)); const std::int32_t result = sceFiberRun_nid_postfix(g_first, 100, returned); @@ -210,4 +237,5 @@ int main() { Require(sceFiberStopContextSizeCheck() == 0, "stop size check"); RunHostCallOnSmallContext(); + RunFrameChain(); } From 6ad9372fdde615ef8530f527a118211d0dad325f Mon Sep 17 00:00:00 2001 From: "john.maia" <johnmaia@gmail.com> Date: Tue, 13 Oct 2026 11:49:12 +0100 Subject: [PATCH 19/81] fix(agc): store a storage image whose last block row runs past its allocation A storage image whose surface runs past the end of its registered allocation had every write-back dropped as "memory no longer registered". Titles can size a single-level surface's allocation as pitch x height. A tiled mip's 64 KiB blocks are stored row-major, so that size ends inside the last block row, and the bytes after it are the tail of that row: they hold texels with y < height, which the GPU's own store writes into the next allocation. When the allocation ends inside the last block row and registered readable memory continues to the surface's end, the write-back now stores the whole surface, as the GPU does. Padding slots keep their bytes, since the store writes only bytes that changed. Epic Mickey: Rebrushed streams its lightmaps into an 8160x8160 BC3 pool (SW_64KB_S, 2048 block rows) through a 2040x2040 RGBA32UI alias written by compute. The game allocates 2040 rows, so all its write-backs were dropped and the walls sampled leftover memory as speckled noise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../libSceAgcDriver/Graphics/src/Texture.cpp | 17 ++ .../execution/StorageWriteBackOverhang.cpp | 170 ++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackOverhang.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index c6bed0bba93..b3a4bdff979 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -4291,7 +4291,24 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) { // the round trip through the CPU below would store stale texels over whatever the memory // holds now. Its selected units are dropped instead. APS5_NO_UNREGISTERED_DROP=1 stores. static const bool unregisteredDrop = std::getenv("APS5_NO_UNREGISTERED_DROP") == nullptr; + bool overhang = false; if (unregisteredDrop && !RegisteredReadableCovers(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) { + const auto allocationEnd = RegisteredReadableEnd(descriptor.baseAddress); + const auto& level = mips.front(); + const auto rowBytes = std::uint64_t{level.height} * level.pitchBytes; + const auto lastRowTail = mips.size() == 1 && arrayLayers == 1 && !level.tail && level.tiledSize > rowBytes ? level.tiledSize - rowBytes : 0; + const auto end = descriptor.baseAddress + guestBytes; + if (allocationEnd > descriptor.baseAddress && end - allocationEnd <= lastRowTail) { + auto covered = allocationEnd; + while (covered < end) { + const auto next = RegisteredReadableEnd(covered); + if (next <= covered) break; + covered = next; + } + overhang = covered >= end; + } + } + if (unregisteredDrop && !overhang && !RegisteredReadableCovers(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) { static std::atomic<int> reports{0}; if (reports.fetch_add(1) < 4) std::fprintf(stderr, "[gpu] storage image 0x%llx+0x%llx: memory no longer registered; %zu pending ranges dropped\n", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), keep.size()); unregisteredDropped.fetch_add(1, std::memory_order_relaxed); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackOverhang.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackOverhang.cpp new file mode 100644 index 00000000000..4f5f7f8c0bd --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackOverhang.cpp @@ -0,0 +1,170 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include <windows.h> +#else +#include <sys/mman.h> +#endif +#include <algorithm> +#include <array> +#include <cstdio> +#include <cstring> +#include <iostream> +#include <span> +#include <string> +#include <vector> + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Width = 128; +constexpr std::uint32_t Height = 120; +constexpr std::uint32_t FormatRgba16Float = 71; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Standard64KB = 0x09; +constexpr std::size_t SurfaceBytes = 0x20000; +constexpr std::size_t AllocationBytes = std::size_t{Width} * Height * 8u; +constexpr std::uint8_t Sentinel = 0xa5; + +alignas(256) constexpr std::array<std::uint32_t, 11> WriteCode{0xd7460004u, 0x0401060eu, 0xd7460005u, 0x0405060fu, 0x7e100d04u, 0x7e120d05u, 0x7e140c0du, 0x7e1602f2u, 0xf0200f08u, 0x00000804u, 0xbf810000u}; + +std::array<std::uint32_t, 8> TextureDescriptor(const void* texels) { + const auto address = reinterpret_cast<std::uintptr_t>(texels); + return { + static_cast<std::uint32_t>(address >> 8u), + static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (FormatRgba16Float << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), + 0xfacu | (Standard64KB << 20u) | (Type2D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +void Write(std::uint8_t* texels) { + std::vector<std::uint32_t> data; + const auto texture = TextureDescriptor(texels); + data.insert(data.end(), texture.begin(), texture.end()); + data.resize(14, 0u); + data[13] = 7u; + std::vector<std::uint32_t> words; + const auto registers = [&](std::uint32_t first, std::span<const std::uint32_t> values) { + words.push_back(0xc0007600u | (static_cast<std::uint32_t>(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array<std::uint32_t, 3> shape{8, 8, 1}; + registers(0x207, shape); + const auto code = reinterpret_cast<std::uintptr_t>(WriteCode.data()); + const std::array<std::uint32_t, 2> program{static_cast<std::uint32_t>(code >> 8u), static_cast<std::uint32_t>(code >> 40u)}; + registers(0x20c, program); + const std::array<std::uint32_t, 1> resource{(static_cast<std::uint32_t>(data.size()) << 1u) | (7u << 7u) | (1u << 11u)}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, Width / 8u, Height / 8u, 1u, 0x41u}); + Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::Graphics::StorageTexture::FlushAllPending("test"); + AgcDriverWaitIdle_nid_postfix(); +} + +class GuestSurface { +public: + explicit GuestSurface(bool split) { +#ifdef _WIN32 + block = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, Alignment)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); +#else + void* mapped = mmap(nullptr, SurfaceBytes + Alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast<std::uintptr_t>(mapped); + const auto aligned = (begin + Alignment - 1) & ~(static_cast<std::uintptr_t>(Alignment) - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + SurfaceBytes != begin + SurfaceBytes + Alignment) munmap(reinterpret_cast<void*>(aligned + SurfaceBytes), begin + Alignment - aligned); + block = reinterpret_cast<std::uint8_t*>(aligned); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, SurfaceBytes); + } +#endif + Require(block != nullptr, "storage write-back overhang: cannot allocate the guest surface"); + std::memset(block, 0, AllocationBytes); + std::memset(block + AllocationBytes, Sentinel, SurfaceBytes - AllocationBytes); + GuestAllocations::Mutation mutation; + if (split) { + mutation.Add(block, AllocationBytes, true, true, true); + mutation.Add(block + AllocationBytes, SurfaceBytes - AllocationBytes, true, true, true); + } else { + mutation.Add(block, SurfaceBytes, true, true, true); + } + this->split = split; + } + + ~GuestSurface() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + if (split) mutation.Remove(block + AllocationBytes); + } +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, SurfaceBytes); +#else + munmap(block, SurfaceBytes); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, SurfaceBytes); +#endif + } + + GuestSurface(const GuestSurface&) = delete; + GuestSurface& operator=(const GuestSurface&) = delete; + + std::uint8_t* Data() { return block; } + +private: + static constexpr std::size_t Alignment = 65536; + std::uint8_t* block = nullptr; + bool split = false; +}; + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + GuestSurface whole(false); + GuestSurface split(true); + Write(whole.Data()); + Write(split.Data()); + const auto* expected = whole.Data(); + const auto* stored = split.Data(); + const auto texel = [](const std::uint8_t* at) { return !std::all_of(at, at + 8, [](std::uint8_t byte) { return byte == 0 || byte == Sentinel; }); }; + std::size_t written = 0; + std::size_t past = 0; + for (std::size_t at = 0; at < SurfaceBytes; at += 8) { + written += texel(expected + at); + past += at >= AllocationBytes && texel(expected + at); + } + Require(written != 0, "storage write-back overhang: the fully registered surface received no texels"); + Require(past != 0, "storage write-back overhang: no texel of the fully registered surface lies past pitch x height"); + const auto differs = std::mismatch(expected, expected + SurfaceBytes, stored); + Require(differs.first == expected + SurfaceBytes, "storage write-back overhang: byte 0x" + [&] { char text[32]; std::snprintf(text, sizeof(text), "%zx", static_cast<std::size_t>(differs.first - expected)); return std::string(text); }() + " differs from the fully registered surface (" + std::to_string(written) + " texels, " + std::to_string(past) + " past pitch x height)"); + Require(std::any_of(stored + AllocationBytes, stored + SurfaceBytes, [](std::uint8_t byte) { return byte == Sentinel; }), "storage write-back overhang: the padding past the image's rows was overwritten"); + AgcDriverShutdown_nid_postfix(); + std::puts("storage write-back overhang tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} From 26c62cc63d404d81392f9f4ab004d3a6cd29fe2e Mon Sep 17 00:00:00 2001 From: SandstromPL <paritoshlahre@gmail.com> Date: Wed, 14 Oct 2026 07:27:54 +0530 Subject: [PATCH 20/81] fix(relinker): give the output's temporary file a unique name A relink killed between writing <output>.tmp and renaming it left that file behind, and every later relink of the output then fell back to the in-place write, bringing back the failure on a running build. The temporary file is now created under a new name beside the output with O_CREAT | O_EXCL and the previous permission bits, and written through that descriptor, so a file left by an earlier run never blocks a replacement and is never touched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/relinker/io/src/FileWriter.cpp | 95 +++++++++++++++------- core/relinker/io/tests/FileWriterTests.cpp | 30 +++++-- 2 files changed, 90 insertions(+), 35 deletions(-) diff --git a/core/relinker/io/src/FileWriter.cpp b/core/relinker/io/src/FileWriter.cpp index 1760af8f974..49df1334111 100644 --- a/core/relinker/io/src/FileWriter.cpp +++ b/core/relinker/io/src/FileWriter.cpp @@ -4,17 +4,24 @@ #include <filesystem> #include <fstream> #include <system_error> +#ifndef _WIN32 +#include <cerrno> +#include <random> +#include <string_view> +#include <fcntl.h> +#include <sys/stat.h> +#include <unistd.h> +#endif namespace Io { namespace { -template<typename TFill> -bool writeFile(const std::filesystem::path& file, const std::string& path, const std::ios::openmode mode, const TFill& fill) { +bool writeFile(const std::filesystem::path& file, const std::string& path, const std::ios::openmode mode, const char* data, const std::size_t size) { std::ofstream f(file, mode); if (!f) return false; - fill(f); + f.write(data, static_cast<std::streamsize>(size)); f.close(); if (!f) throw Domain::RelinkerException("Failed to write file: " + path); @@ -22,12 +29,57 @@ bool writeFile(const std::filesystem::path& file, const std::string& path, const } #ifndef _WIN32 -bool moveOver(const std::filesystem::path& temporary, const std::filesystem::path& target, const std::filesystem::file_status& previous) { +int createTemporary(const std::filesystem::path& target, const mode_t mode, std::filesystem::path& temporary) { + constexpr std::string_view characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + std::random_device device; + for (int attempt = 0; attempt < 100; ++attempt) { + std::string suffix = "."; + for (int index = 0; index < 6; ++index) + suffix += characters[device() % characters.size()]; + temporary = target; + temporary += suffix; + const int descriptor = open(temporary.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, mode); + if (descriptor >= 0 || errno != EEXIST) + return descriptor; + } + return -1; +} + +bool writeAll(const int descriptor, const char* data, std::size_t size) { + while (size != 0) { + const auto written = write(descriptor, data, size); + if (written < 0 && errno == EINTR) + continue; + if (written <= 0) + return false; + data += written; + size -= static_cast<std::size_t>(written); + } + return true; +} + +bool replaceFile(const std::filesystem::path& target, const std::string& path, const char* data, const std::size_t size) { std::error_code error; - if (std::filesystem::is_regular_file(previous)) - std::filesystem::permissions(temporary, previous.permissions() & std::filesystem::perms::all, error); - if (!error) - std::filesystem::rename(temporary, target, error); + const auto 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; + const auto mode = regular ? static_cast<mode_t>(previous.permissions() & std::filesystem::perms::all) : static_cast<mode_t>(0666); + std::filesystem::path temporary; + const int descriptor = createTemporary(target, mode, temporary); + if (descriptor < 0) + return false; + if (regular && fchmod(descriptor, mode) != 0) { + close(descriptor); + std::filesystem::remove(temporary, error); + return false; + } + const bool written = writeAll(descriptor, data, size); + if (close(descriptor) != 0 || !written) { + std::filesystem::remove(temporary, error); + throw Domain::RelinkerException("Failed to write file: " + path); + } + std::filesystem::rename(temporary, target, error); if (!error) return true; std::filesystem::remove(temporary, error); @@ -35,39 +87,24 @@ bool moveOver(const std::filesystem::path& temporary, const std::filesystem::pat } #endif -template<typename TFill> -void replaceFile(const std::string& path, const std::ios::openmode mode, const TFill& fill) { +void writeOutput(const std::string& path, const std::ios::openmode mode, const char* data, const std::size_t size) { const auto target = NativePath(path); #ifndef _WIN32 - std::error_code error; - const auto previous = std::filesystem::symlink_status(target, error); - auto temporary = target; - temporary += ".tmp"; - if ((std::filesystem::is_regular_file(previous) || previous.type() == std::filesystem::file_type::not_found) - && std::filesystem::symlink_status(temporary, error).type() == std::filesystem::file_type::not_found) { - try { - if (writeFile(temporary, path, mode, fill) && moveOver(temporary, target, previous)) - return; - } catch (...) { - std::filesystem::remove(temporary, error); - throw; - } - } + if (replaceFile(target, path, data, size)) + return; #endif - if (!writeFile(target, path, mode, fill)) + if (!writeFile(target, path, mode, data, size)) throw Domain::RelinkerException("Cannot open output file: " + path); } } void FileWriter::Write(const std::string& path, const std::vector<std::uint8_t>& data) { - replaceFile(path, std::ios::binary, [&data](std::ofstream& f) { - f.write(reinterpret_cast<const char*>(data.data()), static_cast<std::streamsize>(data.size())); - }); + writeOutput(path, std::ios::binary, reinterpret_cast<const char*>(data.data()), data.size()); } void FileWriter::Write(const std::string& path, const std::string& content) { - replaceFile(path, std::ios::out, [&content](std::ofstream& f) { f << content; }); + writeOutput(path, std::ios::out, content.data(), content.size()); } } diff --git a/core/relinker/io/tests/FileWriterTests.cpp b/core/relinker/io/tests/FileWriterTests.cpp index 3ca74275984..900518b16f3 100644 --- a/core/relinker/io/tests/FileWriterTests.cpp +++ b/core/relinker/io/tests/FileWriterTests.cpp @@ -11,6 +11,7 @@ #include <vector> #include <fcntl.h> #include <sys/resource.h> +#include <sys/stat.h> #include <sys/wait.h> #include <unistd.h> @@ -35,11 +36,23 @@ std::string readFile(const std::filesystem::path& path) { return {std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()}; } +std::vector<std::string> siblings(const std::filesystem::path& output) { + std::vector<std::string> names; + for (const auto& entry : std::filesystem::directory_iterator(output.parent_path())) { + const auto name = entry.path().filename().string(); + if (name.starts_with(output.filename().string() + ".")) + names.push_back(name); + } + return names; +} + template<typename TValue> -void requireRunningReplacement(const std::filesystem::path& directory, const TValue& data) { - const auto output = directory / "running"; +void requireRunningReplacement(const std::filesystem::path& directory, const TValue& data, const bool leftover) { + const auto output = directory / (leftover ? "leftover" : "running"); + if (leftover) + std::ofstream(directory / "leftover.tmp", std::ios::binary) << "killed"; std::filesystem::copy_file("/bin/sleep", output); - const auto permissions = std::filesystem::perms::owner_all | std::filesystem::perms::group_read | std::filesystem::perms::group_exec; + const auto permissions = std::filesystem::perms::owner_all | std::filesystem::perms::group_all; std::filesystem::permissions(output, permissions); const auto previous = readFile(output); const pid_t child = fork(); @@ -59,11 +72,13 @@ void requireRunningReplacement(const std::filesystem::path& directory, const TVa const int descriptor = open(output.c_str(), O_RDONLY); std::string failure; if (started) { + const auto mask = umask(077); try { Io::FileWriter().Write(output.string(), data); } catch (const std::exception& e) { failure = e.what(); } + umask(mask); } std::string kept(previous.size(), '\0'); const bool keptPrevious = pread(descriptor, kept.data(), kept.size(), 0) == static_cast<ssize_t>(kept.size()) && kept == previous; @@ -85,8 +100,10 @@ void requireRunningReplacement(const std::filesystem::path& directory, const TVa throw std::runtime_error("An open descriptor no longer reads the previous file"); if (!running) throw std::runtime_error("Running executable stopped"); - if (std::filesystem::exists(directory / "running.tmp")) + if (siblings(output) != (leftover ? std::vector<std::string>{"leftover.tmp"} : std::vector<std::string>{})) throw std::runtime_error("Temporary file left behind"); + if (leftover && readFile(directory / "leftover.tmp") != "killed") + throw std::runtime_error("A file left by an earlier run was changed"); } template<typename TValue> @@ -109,7 +126,7 @@ void requireFailedReplacement(const std::filesystem::path& directory, const TVal std::signal(SIGXFSZ, handler); if (failure != "Failed to write file: " + output.string()) throw std::runtime_error("Write past the file size limit did not fail: " + failure); - if (readFile(output) != "previous" || std::filesystem::exists(directory / "limited.tmp")) + if (readFile(output) != "previous" || !siblings(output).empty()) throw std::runtime_error("Failed write did not keep the previous file"); } @@ -138,7 +155,8 @@ void requireReplacement(const TValue& data) { std::filesystem::remove_all(directory, error); std::filesystem::create_directory(directory); try { - requireRunningReplacement(directory, data); + requireRunningReplacement(directory, data, false); + requireRunningReplacement(directory, data, true); requireFailedReplacement(directory, data); requireInPlaceWrites(directory, data); } catch (...) { From 11ef8e6c837fe09116757d104332d6f4fcc01c6a Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Tue, 13 Oct 2026 03:53:19 -0400 Subject: [PATCH 21/81] fix(agc): settle registration preparations before a preparation scope locks sceAgcCreateInterpolantMapping resolved the pixel shader's ABI, which takes the process-wide shader preparation lock until the scope ends, and then waited under that lock for the vertex shader's background registration preparation. sceAgcCreatePrimState and sceAgcLinkShaders could wait the same way. Every other shader registration and ABI resolution queued behind that one compile: on PPSA07411 the lock was held 8 to 10 s of every 10 s, almost all of it waiting for registration preparations, while its loader threads waited 13 to 45 s per 10 s to take it. A preparation scope now names the shaders it resolves and waits for their registration preparations before it can take the lock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../Shader/src/InterpolantMapping.cpp | 2 +- .../prx/libSceAgc/Shader/src/LinkShaders.cpp | 2 +- .../prx/libSceAgc/Shader/src/PrimState.cpp | 3 + core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 + .../Execution/include/Driver/Driver.hpp | 1 + .../include/ShaderPreparationScope.hpp | 9 +- .../src/Driver/Shaders/ShaderRegistry.cpp | 19 ++- .../execution/PreparationScopeSettle.cpp | 150 ++++++++++++++++++ 8 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PreparationScopeSettle.cpp diff --git a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp index 608c859ad89..1f3c3ac3c67 100644 --- a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp @@ -72,7 +72,7 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* throw std::runtime_error(std::string(fn) + ": input semantic count exceeds 32"); } - AgcDriver::ShaderPreparationScope transaction; + AgcDriver::ShaderPreparationScope transaction({ps != nullptr ? gs : nullptr, ps}); std::array<ShaderRegister, 32> values{}; auto* output = regs; regs = values.data(); diff --git a/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp b/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp index 1044b410f44..59a43354c8b 100644 --- a/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp @@ -22,7 +22,7 @@ extern "C" int APS5_VABI sceAgcLinkShaders(ShaderRegister* context, ShaderRegist // The simple vertex/pixel pipeline combines two primitive context registers // and 32 interpolant registers. Publish only after both helpers succeed. - AgcDriver::ShaderPreparationScope transaction; + AgcDriver::ShaderPreparationScope transaction({vertex, pixel}); std::array<ShaderRegister, 34> contextValues{}; std::array<ShaderRegister, 3> primitiveValues{}; sceAgcCreatePrimState(contextValues.data(), primitiveValues.data(), nullptr, vertex, primitiveType); diff --git a/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp b/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp index 587ae15a320..b62666315c9 100644 --- a/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp @@ -8,6 +8,7 @@ #include "SceShaders.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp" #include "prx/libSceAgc/Shader/include/ShaderUtils.hpp" #include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" @@ -56,7 +57,9 @@ int APS5_VABI sceAgcCreatePrimState(ShaderRegister* cx_regs, ShaderRegister* uc_ const std::array<const Shader*, 2> stages{hs, gs}; const bool patch = prim_type == static_cast<std::uint32_t>(ShaderRegs::PrimitiveType::Patch); if (patch == ((contextValues[0].value & ShaderRegs::VGT_SHADER_STAGES_HS_BIT) != 0)) { + AgcDriver::ShaderPreparationScope transaction({hs, gs}); AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span(stages).subspan(hs == nullptr ? 1u : 0u), contextValues, primitiveValues); + transaction.Commit(); } if (cx_regs != nullptr) std::copy(contextValues.begin(), contextValues.end(), cx_regs); if (uc_regs != nullptr) std::copy(primitiveValues.begin(), primitiveValues.end(), uc_regs); diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 3fdab7d9a8d..48f41dfd5d2 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1155,6 +1155,8 @@ endforeach() target_link_libraries(agc_driver_gpu_map_on_fiber_tests PRIVATE libSceFiber) +target_link_libraries(agc_driver_preparation_scope_settle_tests PRIVATE libSceAgc) + target_include_directories(agc_driver_alpha_to_coverage_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>) target_include_directories(agc_driver_dual_source_blend_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index c305a1c9337..002cc4f5246 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -61,6 +61,7 @@ class Driver { void ResolveShaderAbi(const Shader* shader, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive); void ResolveGraphicsStagesAbi(std::span<const Shader* const> stages, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive); void ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType); + void SettleShaderPreparations(std::span<const Shader* const> stages); static void FoldDrawOffsets(const ShaderRecompiler::RecompileResult& result, const DrawProgram& program, Pm4::DrawParameters& parameters); static std::optional<Graphics::IndirectDrawPath> ClassifyIndirectDraw(const ShaderRecompiler::RecompileResult& result, const Graphics::State& graphics, const DrawProgram& frontProgram, const std::shared_ptr<VulkanDevice>& localDevice, Pm4::DrawParameters& drawParameters, bool traceIndirect); static std::string dumpRequest(std::uint64_t address, const ShaderRecompiler::RecompileRequest& request); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp index 0c314803071..30fcf5b8f43 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp @@ -1,11 +1,16 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SHADERPREPARATIONSCOPE_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SHADERPREPARATIONSCOPE_HPP +#include <cstddef> +#include <initializer_list> + +struct Shader; + namespace AgcDriver::DriverDetail { class ShaderPreparationTransaction; } -extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix(); +extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix(const Shader* const* shaders, std::size_t count); extern "C" void AgcDriverCommitShaderPreparation_nid_postfix(AgcDriver::DriverDetail::ShaderPreparationTransaction* transaction); extern "C" void AgcDriverEndShaderPreparation_nid_postfix(AgcDriver::DriverDetail::ShaderPreparationTransaction* transaction) noexcept; @@ -13,7 +18,7 @@ namespace AgcDriver { class ShaderPreparationScope { public: - ShaderPreparationScope() : transaction(AgcDriverBeginShaderPreparation_nid_postfix()) {} + explicit ShaderPreparationScope(std::initializer_list<const Shader*> shaders) : transaction(AgcDriverBeginShaderPreparation_nid_postfix(shaders.begin(), shaders.size())) {} ~ShaderPreparationScope() { AgcDriverEndShaderPreparation_nid_postfix(transaction); } ShaderPreparationScope(const ShaderPreparationScope&) = delete; ShaderPreparationScope& operator=(const ShaderPreparationScope&) = delete; 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 4ad3671f979..80578b7c6da 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1180,6 +1180,22 @@ void Driver::ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std:: transaction.Commit(); } +void Driver::SettleShaderPreparations(std::span<const Shader* const> stages) { + std::vector<std::shared_ptr<const ShaderSnapshot>> snapshots; + { + std::lock_guard lock(mutex); + if (shaders == nullptr) return; + for (const auto* shader : stages) { + if (shader == nullptr) continue; + if (auto snapshot = RegisteredHeader(*shaders, shader)) snapshots.push_back(std::move(snapshot)); + } + } + for (const auto& snapshot : snapshots) { + std::unique_lock lock(snapshot->prepared->mutex); + AwaitRegisteredPreparation(*snapshot->prepared, lock); + } +} + void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr<const ShaderSnapshot>& fragment, std::uint32_t primitiveType, const ShaderRecompiler::SpirvTarget& target) { ShaderPreparationTransaction transaction; require(fragment.get() != &front, "rectangle stages refer to the same shader"); @@ -1350,7 +1366,8 @@ void Driver::RegisterShader(const Shader* shader) { } -extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix() { +extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix(const Shader* const* shaders, std::size_t count) { + AgcDriver::DriverDetail::Driver::Get().SettleShaderPreparations({shaders, count}); return new AgcDriver::DriverDetail::ShaderPreparationTransaction(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparationScopeSettle.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparationScopeSettle.cpp new file mode 100644 index 00000000000..2233d80ba37 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparationScopeSettle.cpp @@ -0,0 +1,150 @@ +#include "VulkanTestDevice.hpp" +#include "SceShaders.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include <array> +#include <chrono> +#include <condition_variable> +#include <cstdint> +#include <future> +#include <iostream> +#include <memory> +#include <mutex> +#include <stdexcept> +#include <thread> + +extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Shader* gs, const Shader* ps); + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) const std::array<std::uint32_t, 11> VertexCode{0x7e0002ffu, 0, 0x7e0202ffu, 0, 0x7e0402ffu, 0, 0x7e0602ffu, 0x3f800000u, 0xf80008cfu, 0x03020100u, 0xbf810000u}; +alignas(256) const std::array<std::uint32_t, 1> PixelCode{0xbf810000u}; + +struct VertexHeader { + Shader shader{}; + std::array<ShaderRegister, 7> registers{}; + ShaderSpecialRegs specials{}; + ShaderUserData users{}; + std::array<ShaderRegister, 1> context{{{0x2d5, 0x2000}}}; + + VertexHeader() { + const auto address = reinterpret_cast<std::uintptr_t>(VertexCode.data()); + shader.file_header = 0x34333231u; + shader.version = 0x18; + shader.header_size = sizeof(VertexHeader); + shader.shader_size = sizeof(VertexCode); + shader.code = VertexCode.data(); + shader.type = 2; + registers = {{{0xc8, static_cast<std::uint32_t>(address >> 8u)}, {0xc9, static_cast<std::uint32_t>(address >> 40u)}, {0x8b, 0}, {0x8a, 0}, {0xca, 0}, {0xcb, 0}, {0xcc, 0}}}; + shader.sh_registers = registers.data(); + shader.num_sh_registers = registers.size(); + shader.cx_registers = context.data(); + shader.num_cx_registers = context.size(); + shader.specials = &specials; + shader.user_data = &users; + specials.dispatch_modifier = 0x8000; + } +}; + +struct PixelHeader { + Shader shader{}; + std::array<ShaderRegister, 4> registers{}; + std::array<ShaderRegister, 3> context{{{0x1b3, 2}, {0x1b4, 2}, {0x1b6, 1}}}; + + PixelHeader() { + const auto address = reinterpret_cast<std::uintptr_t>(PixelCode.data()); + shader.file_header = 0x34333231u; + shader.version = 0x18; + shader.header_size = sizeof(PixelHeader); + shader.shader_size = sizeof(PixelCode); + shader.code = PixelCode.data(); + shader.type = 1; + registers = {{{0x008, static_cast<std::uint32_t>(address >> 8u)}, {0x009, static_cast<std::uint32_t>(address >> 40u)}, {0x00a, 0}, {0x00b, 0}}}; + shader.sh_registers = registers.data(); + shader.num_sh_registers = registers.size(); + shader.cx_registers = context.data(); + shader.num_cx_registers = context.size(); + } +}; + +class HeldPreparationThreads { +public: + HeldPreparationThreads() { + for (unsigned index = 0; index < std::thread::hardware_concurrency() + 2u; ++index) { + AgcDriver::DriverDetail::SubmitRegistrationPreparation([state = state] { + std::unique_lock lock(state->mutex); + state->changed.wait(lock, [&] { return state->released; }); + }); + } + } + + ~HeldPreparationThreads() { + Release(); + } + + void Release() { + { + std::lock_guard lock(state->mutex); + state->released = true; + } + state->changed.notify_all(); + } + +private: + struct State { + std::mutex mutex; + std::condition_variable changed; + bool released = false; + }; + std::shared_ptr<State> state = std::make_shared<State>(); +}; + +void RegistrationBesidePendingLink() { + VertexHeader vertex; + VertexHeader other; + PixelHeader pixel; + AgcDriverRegisterShader_nid_postfix(&pixel.shader); + std::future<int> link; + std::future<void> registration; + bool registeredBeside = false; + std::array<ShaderRegister, 32> mapping{}; + { + HeldPreparationThreads held; + AgcDriverRegisterShader_nid_postfix(&vertex.shader); + link = std::async(std::launch::async, [&] { return sceAgcCreateInterpolantMapping_0100(mapping.data(), &vertex.shader, &pixel.shader); }); + std::this_thread::sleep_for(std::chrono::seconds(1)); + Require(link.wait_for(std::chrono::seconds(0)) == std::future_status::timeout, "the link did not wait for the vertex shader's registration preparation"); + registration = std::async(std::launch::async, [&] { AgcDriverRegisterShader_nid_postfix(&other.shader); }); + registeredBeside = registration.wait_for(std::chrono::seconds(5)) == std::future_status::ready; + held.Release(); + } + registration.get(); + Require(link.get() == 0, "the interpolant mapping failed"); + Require(registeredBeside, "a link waiting for a registration preparation blocked the registration of another shader"); + for (std::uint32_t index = 0; index < mapping.size(); ++index) Require(mapping[index].offset == 0x191u + index, "the interpolant mapping wrote another register"); +} + +} + +int main() { + try { + if (!OpenVulkanTestDevice()) return VulkanTestSkipped; + RegistrationBesidePendingLink(); + AgcDriverShutdown_nid_postfix(); + std::cout << "preparation scope settle tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception& shutdownError) { + std::cerr << shutdownError.what() << '\n'; + } + return 1; + } +} From 2251649a7f43a1e9fa98227381af42320c2431cb Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Mon, 12 Oct 2026 15:25:18 -0400 Subject: [PATCH 22/81] build(libs): keep import libraries stable so a source edit relinks one library Every library under core/libs/prx links the libc and libkernel import libraries, and every NID patch step depended on the unpatched libc.prx. GNU ld 2.46 rewrites the import library on every link with new archive timestamps (the member headers and the symbol map header), so the restat on the link edge never pruned anything: one libc or libkernel .cpp edit relinked all 124 libraries and re-patched all of them. On MinGW the shared library link now writes the import library to <implib>.new with SOURCE_DATE_EPOCH=0 (ld then writes zero member timestamps), makes the symbol map deterministic with ranlib -D and copies the result over the import library only when its bytes differ (cmake -E copy_if_different). The link edge keeps restat, so a link that keeps the exports leaves the import library untouched and prunes every dependent link. The SOURCE_DATE_EPOCH also zeroes the export directory TimeDateStamp of the DLL; the COFF header TimeDateStamp that the unwinder keys on is unchanged. The NID patch steps only use the names libc exports. nid_patcher --write-export-list writes those names, normalized and sorted, to libc.exports and leaves the file untouched when they did not change; the patch steps read it with --preserve-export-list and depend on it instead of libc.prx, on every platform. A patched library is byte-identical to one patched against libc.prx. Measured on Windows (GCC 15.2, -j 4, loaded machine) after touching libc/src/Strings.cpp: with CMAKE_LINK_DEPENDS_NO_SHARED off, 124 links and 124 patches in 74.9 s before, 1 compile, 1 link, the export list and the libc patch in 4.3 s after; with it on, 124 patches in 7.8 s before and the same four steps in 5.3 s after. Touching libkernel/Time/Time.cpp runs 123 links and 123 patches (89.1 s) before and one compile, link and patch (3.4 s) after. Removing a libc export still relinks and re-patches all 124 libraries. nid_patcher_macho covers --write-export-list (sorted normalized names, no rewrite when unchanged, rewrite when changed) and --preserve-export-list (the same exports kept as with --preserve-exports; malformed lists and two references refused). It fails on main, where the option is unknown. Sources: SOURCE_DATE_EPOCH, https://reproducible-builds.org/specs/source-date-epoch/; ranlib -D and ld --out-implib, GNU binutils documentation; ninja restat, https://ninja-build.org/manual.html#ref_rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/CMakeLists.txt | 27 +++++++++- .../libs/nid/include/nid/ExportExclusions.hpp | 2 + core/libs/nid/src/ExportExclusions.cpp | 48 ++++++++++++++++++ core/libs/nid/src/main.cpp | 23 +++++++-- core/libs/nid/tests/test_macho_nids.py | 49 +++++++++++++++++++ docs/dev/ARCHITECTURE.md | 2 +- 6 files changed, 144 insertions(+), 7 deletions(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 707f6682611..d3f4ad4d417 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -52,6 +52,21 @@ if(MINGW) foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") endforeach() + + if(NOT EXISTS "${CMAKE_RANLIB}") + message(FATAL_ERROR "Stable import libraries require ranlib") + endif() + list(LENGTH CMAKE_CXX_CREATE_SHARED_LIBRARY linkCommandCount) + string(FIND "${CMAKE_CXX_CREATE_SHARED_LIBRARY}" "-Wl,--out-implib,<TARGET_IMPLIB> " implibOption) + if(NOT linkCommandCount EQUAL 1 OR implibOption EQUAL -1) + message(FATAL_ERROR "Expected one shared library link command that writes -Wl,--out-implib,<TARGET_IMPLIB>: ${CMAKE_CXX_CREATE_SHARED_LIBRARY}") + endif() + string(REPLACE "-Wl,--out-implib,<TARGET_IMPLIB> " "-Wl,--out-implib,<TARGET_IMPLIB>.new " linkCommand "${CMAKE_CXX_CREATE_SHARED_LIBRARY}") + set(CMAKE_CXX_CREATE_SHARED_LIBRARY + "\"${CMAKE_COMMAND}\" -E env SOURCE_DATE_EPOCH=0 ${linkCommand}" + "\"${CMAKE_RANLIB}\" -D <TARGET_IMPLIB>.new" + "\"${CMAKE_COMMAND}\" -E copy_if_different <TARGET_IMPLIB>.new <TARGET_IMPLIB>" + ) endif() set(LIBC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/prx/libc) @@ -81,6 +96,14 @@ set_target_properties(libc PROPERTIES set(LIBC_UNPATCHED ${UNPATCHED_DIR}/libc.prx) set(LIBC_PATCHED ${PATCHED_DIR}/libc.prx) +set(LIBC_EXPORT_LIST ${CMAKE_CURRENT_BINARY_DIR}/libc.exports) + +add_custom_command( + OUTPUT ${LIBC_EXPORT_LIST} + COMMAND $<TARGET_FILE:nid_patcher> --write-export-list ${LIBC_UNPATCHED} ${LIBC_EXPORT_LIST} + DEPENDS nid_patcher libc ${LIBC_UNPATCHED} + COMMENT "Listing libc exports" +) add_custom_command( OUTPUT ${LIBC_PATCHED} @@ -140,8 +163,8 @@ foreach(LIB_DIR ${LIBS_DIRS}) add_custom_command( OUTPUT ${PATCHED_FILE} COMMAND ${CMAKE_COMMAND} -E copy ${UNPATCHED_FILE} ${PATCHED_FILE} - COMMAND $<TARGET_FILE:nid_patcher> ${LIB_NAME} --preserve-exports "${LIBC_UNPATCHED}" "${PATCHED_FILE}" - DEPENDS nid_patcher ${LIB_NAME} ${UNPATCHED_FILE} ${LIBC_UNPATCHED} + COMMAND $<TARGET_FILE:nid_patcher> ${LIB_NAME} --preserve-export-list "${LIBC_EXPORT_LIST}" "${PATCHED_FILE}" + DEPENDS nid_patcher ${LIB_NAME} ${UNPATCHED_FILE} ${LIBC_EXPORT_LIST} COMMENT "Patching NIDs for ${LIB_NAME}" ) diff --git a/core/libs/nid/include/nid/ExportExclusions.hpp b/core/libs/nid/include/nid/ExportExclusions.hpp index 93442d156e7..ae41e169c54 100644 --- a/core/libs/nid/include/nid/ExportExclusions.hpp +++ b/core/libs/nid/include/nid/ExportExclusions.hpp @@ -8,6 +8,8 @@ namespace Nid { std::string NormalizeExportName(const std::string& name); std::unordered_set<std::string> ReadExportExclusions(const std::string& path); +std::unordered_set<std::string> ReadExportList(const std::string& path); +bool WriteExportList(const std::string& referencePath, const std::string& path); } diff --git a/core/libs/nid/src/ExportExclusions.cpp b/core/libs/nid/src/ExportExclusions.cpp index 366e918cb55..273f517d3b7 100644 --- a/core/libs/nid/src/ExportExclusions.cpp +++ b/core/libs/nid/src/ExportExclusions.cpp @@ -4,10 +4,13 @@ #include <nid/MachONidPatcher.hpp> #include <nid/NidPatcherUtils.hpp> #include <nid/NativePath.hpp> +#include <algorithm> #include <bit> #include <cstring> #include <fstream> +#include <iterator> #include <stdexcept> +#include <vector> namespace Nid { @@ -113,6 +116,24 @@ std::unordered_set<std::string> ReadElfExports(const std::vector<std::uint8_t>& return result; } +std::string ReadText(const std::string& path, std::ifstream& input) { + std::string text{std::istreambuf_iterator<char>(input), {}}; + if (input.bad()) throw std::runtime_error("cannot read export list: " + path); + return text; +} + +std::string FormatExportList(const std::unordered_set<std::string>& exports) { + std::vector<std::string> names(exports.begin(), exports.end()); + std::ranges::sort(names); + std::string text; + for (const auto& name : names) { + if (name.find('\n') != std::string::npos) throw std::runtime_error("export name contains a line break: " + name); + text += name; + text += '\n'; + } + return text; +} + } std::string NormalizeExportName(const std::string& name) { @@ -153,4 +174,31 @@ std::unordered_set<std::string> ReadExportExclusions(const std::string& path) { return result; } +std::unordered_set<std::string> ReadExportList(const std::string& path) { + std::ifstream input(path, std::ios::binary); + if (!input) throw std::runtime_error("cannot open export list: " + path); + const auto text = ReadText(path, input); + if (text.empty() || text.back() != '\n') throw std::runtime_error("malformed export list: " + path); + std::unordered_set<std::string> result; + for (std::size_t start = 0; start < text.size();) { + const auto end = text.find('\n', start); + if (end == start) throw std::runtime_error("empty name in export list: " + path); + auto name = text.substr(start, end - start); + if (!result.insert(name).second) throw std::runtime_error("duplicate name in export list: " + name); + start = end + 1; + } + return result; +} + +bool WriteExportList(const std::string& referencePath, const std::string& path) { + const auto text = FormatExportList(ReadExportExclusions(referencePath)); + if (std::ifstream existing(path, std::ios::binary); existing && ReadText(path, existing) == text) return false; + std::ofstream output(path, std::ios::binary | std::ios::trunc); + if (!output) throw std::runtime_error("cannot write export list: " + path); + output.write(text.data(), static_cast<std::streamsize>(text.size())); + output.close(); + if (!output) throw std::runtime_error("cannot write export list: " + path); + return true; +} + } diff --git a/core/libs/nid/src/main.cpp b/core/libs/nid/src/main.cpp index c07a4e8bfaa..7e459dc99d1 100644 --- a/core/libs/nid/src/main.cpp +++ b/core/libs/nid/src/main.cpp @@ -25,10 +25,25 @@ void _writeFile(const std::string& path, const std::vector<std::uint8_t>& data) int Run(int argc, char* argv[]) { if (argc < 3) { - std::cerr << "Usage: nid_patcher <library_name> [--preserve-exports <unpatched-library>] <file> [file2 ...]\n"; + std::cerr << "Usage: nid_patcher <library_name> [--preserve-exports <unpatched-library> | --preserve-export-list <export-list>] <file> [file2 ...]\n" + " nid_patcher --write-export-list <unpatched-library> <export-list>\n"; return 1; } + if (std::string(argv[1]) == "--write-export-list") { + if (argc != 4) { + std::cerr << "FAIL: --write-export-list requires a reference library and an export list\n"; + return 2; + } + try { + std::cout << (Nid::WriteExportList(argv[2], argv[3]) ? "WROTE: " : "UNCHANGED: ") << argv[3] << "\n"; + } catch (const std::exception& e) { + std::cerr << "FAIL: " << argv[3] << ": " << e.what() << "\n"; + return 2; + } + return 0; + } + const std::string libraryName = argv[1]; std::unordered_set<std::string> excludedExports; std::vector<std::string> paths; @@ -36,10 +51,10 @@ int Run(int argc, char* argv[]) { bool hasReference = false; for (int i = 2; i < argc; ++i) { const std::string argument = argv[i]; - if (argument == "--preserve-exports") { - if (hasReference || i + 1 == argc) throw std::runtime_error("--preserve-exports requires exactly one reference library"); + if (argument == "--preserve-exports" || argument == "--preserve-export-list") { + if (hasReference || i + 1 == argc) throw std::runtime_error(argument + " requires exactly one reference"); hasReference = true; - excludedExports = Nid::ReadExportExclusions(argv[++i]); + excludedExports = argument == "--preserve-exports" ? Nid::ReadExportExclusions(argv[++i]) : Nid::ReadExportList(argv[++i]); } else { if (argument.starts_with("--")) throw std::runtime_error("unknown option: " + argument); paths.push_back(argument); diff --git a/core/libs/nid/tests/test_macho_nids.py b/core/libs/nid/tests/test_macho_nids.py index 1d5c118ced3..a4a239fcbc2 100644 --- a/core/libs/nid/tests/test_macho_nids.py +++ b/core/libs/nid/tests/test_macho_nids.py @@ -1,6 +1,7 @@ """Check nid_patcher on small byte-built Mach-O dylibs: the export trie, the chained imports and what it refuses.""" import hashlib +import os import struct import subprocess import sys @@ -211,6 +212,53 @@ def check_preserved(patcher, directory): assert exports_of(path.read_bytes()) == {"_puts_nid_postfix": (0, 0x100), "_" + nid("exit"): (0, 0x108)}, exports_of(path.read_bytes()) +def write_export_list(patcher, reference, listing): + return subprocess.run([str(patcher), "--write-export-list", str(reference), str(listing)], capture_output=True, text=True, timeout=30) + + +def check_export_list(patcher, directory): + reference = Path(directory) / "listed.dylib" + listing = Path(directory) / "listed.exports" + reference.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit", 0, 0x108)], [])) + result = write_export_list(patcher, reference, listing) + assert result.returncode == 0, (result.stdout, result.stderr) + assert listing.read_bytes() == b"exit\nputs\n", listing.read_bytes() + + os.utime(listing, ns=(1_000_000_000_000_000_000, 1_000_000_000_000_000_000)) + reference.write_bytes(build_dylib([("_exit", 0, 0x110), ("_puts_nid_postfix", 0, 0x118)], [])) + result = write_export_list(patcher, reference, listing) + assert result.returncode == 0, (result.stdout, result.stderr) + assert listing.stat().st_mtime_ns == 1_000_000_000_000_000_000, "an unchanged export list was rewritten" + + reference.write_bytes(build_dylib([("_puts", 0, 0x100)], [])) + result = write_export_list(patcher, reference, listing) + assert result.returncode == 0, (result.stdout, result.stderr) + assert listing.read_bytes() == b"puts\n", listing.read_bytes() + assert listing.stat().st_mtime_ns != 1_000_000_000_000_000_000, "a changed export list kept its time" + + path = Path(directory) / "listed-preserve.dylib" + path.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit_nid_postfix", 0, 0x108)], [])) + result = subprocess.run([str(patcher), "libtest", "--preserve-export-list", str(listing), str(path)], capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (result.stdout, result.stderr) + assert exports_of(path.read_bytes()) == {"_puts_nid_postfix": (0, 0x100), "_" + nid("exit"): (0, 0x108)}, exports_of(path.read_bytes()) + + malformed_lists = [ + ("unterminated", b"puts", "malformed export list"), + ("blank", b"puts\n\nexit\n", "empty name in export list"), + ("twice", b"puts\nputs\n", "duplicate name in export list"), + ("empty", b"", "malformed export list"), + ] + for name, content, message in malformed_lists: + malformed = Path(directory) / (name + ".exports") + malformed.write_bytes(content) + target = Path(directory) / (name + ".dylib") + target.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100)], [])) + result = subprocess.run([str(patcher), "libtest", "--preserve-export-list", str(malformed), str(target)], capture_output=True, text=True, timeout=30) + assert result.returncode != 0 and message in result.stdout + result.stderr, (name, result.stdout, result.stderr) + result = subprocess.run([str(patcher), "libtest", "--preserve-exports", str(reference), "--preserve-export-list", str(listing), str(path)], capture_output=True, text=True, timeout=30) + assert result.returncode != 0 and "exactly one reference" in result.stdout + result.stderr, (result.stdout, result.stderr) + + def check_refusals(patcher, directory): growing = [("_ab", 0, 0x100), ("_cd", 0, 0x108)] result, _ = patch(patcher, directory, "signed.dylib", build_dylib(growing, [], signed=True)) @@ -227,6 +275,7 @@ def main(): check_shrinking(patcher, directory) check_growing(patcher, directory) check_preserved(patcher, directory) + check_export_list(patcher, directory) check_refusals(patcher, directory) print("NID patcher Mach-O tests passed") diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 4eaebccf35b..947f13ce35f 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -43,7 +43,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("<nid>", func)` sets the NID directly when the name is unknown. +- 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("<nid>", 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. ## Windows deferred module initialization From dc39e051b90963cb4cbd88362f32b823b603865b Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:36:22 -0400 Subject: [PATCH 23/81] fix(agc): advance stored layers when a write-back keeps CPU-written blocks On a host-imported surface a storage write-back keeps the 64 KiB blocks the CPU wrote since their tracked layer's generation, stores the rest and stamps the stored ranges with MarkWritten. Settling advanced the stored layers' generations past those stamps only when no layer kept a block (or when an adjacent pending surface needed the collect). When one tracked layer kept a block, every other stored layer kept its old generation, so the write-back's own stamps read as CPU writes: the next write-back kept those layers as well and did not store the GPU's newer results over them. A tracked layer is one 64 KiB unit when the surface has block units: tiled (not linear), 64 KiB-aligned base, not thick, with layer and block tracking on. Otherwise it is an array layer (64 KiB-aligned layer slices, layer tracking on) or the whole surface. Settling now collects and advances each stored layer that kept no block; a layer that kept a block keeps its old generation and re-uploads at its next use, as before. When no stored layer advances (nothing stored, or every selected layer kept a block, such as a single tracked layer that kept one), no adjacent pending surface needs the collect and no unit was retiled into the unit shadow, settling still skips the collect, as main did. Units retiled into the unit shadow are now marked at the settle collect's generation also when a block was kept; main marked them at TrackerGeneration() there without collecting. The two differ only for a CPU write to a stored unit made after the keep decision: main left it to a later walk, which made the retiled unit stale so the CPU's bytes won; now the walk stamps it at the marked generation and the stored results win, as they already did when no block was kept. agc_storage_kept_unit_settle uses a 1024x512 RGBA8 surface in 64 KiB tiling (32 block units): it writes one byte of unit 0 from the CPU, stores a red clear, stores a blue clear, and requires units 1-31 to hold blue and unit 0 to keep the CPU's byte after both stores. On main the GPU-mapped direct memory case fails with unit 1 still red. The watched-memory case (host imports off: the write-back that stores through the CPU) passes on main; it guards the kept layer's old generation and fails when settling also advances a layer that kept a block. The test returns 77, naming the reason, when the direct-memory case cannot run: no write watching, no host imports, direct memory not write-watched, or its host import refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 + .../libSceAgcDriver/Graphics/src/Texture.cpp | 20 ++- .../prx/libSceAgcDriver/tests/Recorder.cpp | 135 ++++++++++++++++++ 3 files changed, 150 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 3fdab7d9a8d..9db5f71b641 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -930,6 +930,12 @@ if(BUILD_TESTING) set_tests_properties(agc_unchanged_cpu_stamps PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() + add_test(NAME agc_storage_kept_unit_settle COMMAND agc_driver_recorder_tests --kept-unit-settle-only) + set_tests_properties(agc_storage_kept_unit_settle PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_storage_kept_unit_settle PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() add_test(NAME agc_storage_baseline_settled COMMAND agc_driver_recorder_tests --settled-baseline-only) add_test(NAME agc_storage_integer_clear COMMAND agc_driver_recorder_tests --integer-clear-only) set_tests_properties(agc_storage_integer_clear PROPERTIES SKIP_RETURN_CODE 77) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 703a0e6017c..56089b01026 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -4313,18 +4313,16 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) { for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { if (layers[layer]) layerPending[layer] = false; } - const bool allKept = std::none_of(skippedLayer.begin(), skippedLayer.end(), [](bool kept) { return kept; }); - if (allKept || !adjacent.empty()) { - const auto now = memoizedCollect ? GuestMemory::CollectWrites(firstStored, static_cast<std::size_t>(lastStored - firstStored)) : GuestMemory::CollectWritesUncached(firstStored, static_cast<std::size_t>(lastStored - firstStored)); - for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { - if (layers[layer] && !skippedLayer[layer]) layerGeneration[layer] = now; - } - refreshGeneration(); - advanceAdjacent(adjacent, now, firstBlock, lastBlock); - if (!shadowed.empty()) MarkShadowed(*shadowImport, shadowed, now); - } else if (!shadowed.empty()) { - MarkShadowed(*shadowImport, shadowed, GuestMemory::TrackerGeneration()); + bool advances = false; + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) advances = advances || (layers[layer] && !skippedLayer[layer]); + if (!advances && adjacent.empty() && shadowed.empty()) return; + const auto now = memoizedCollect ? GuestMemory::CollectWrites(firstStored, static_cast<std::size_t>(lastStored - firstStored)) : GuestMemory::CollectWritesUncached(firstStored, static_cast<std::size_t>(lastStored - firstStored)); + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { + if (layers[layer] && !skippedLayer[layer]) layerGeneration[layer] = now; } + refreshGeneration(); + advanceAdjacent(adjacent, now, firstBlock, lastBlock); + if (!shadowed.empty()) MarkShadowed(*shadowImport, shadowed, now); }; if (keep.empty()) { // Every selected block was written by the CPU since the layer's generation: nothing to diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 7f7c8ca0d30..beb762fe229 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -4034,6 +4034,137 @@ void unchangedCpuStampTests(const Device& device, Recorder& recorder) { std::cout << "unchanged CPU stamps: ok\n"; } +bool keptUnitSettleTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + constexpr std::uint32_t width = 1024; + constexpr std::uint32_t height = 512; + constexpr std::size_t surfaceBytes = std::size_t{width} * height * 4; + constexpr std::size_t unitBytes = 65536; + constexpr std::array<std::uint8_t, 4> red{255, 0, 0, 255}; + constexpr std::array<std::uint8_t, 4> blue{0, 0, 255, 255}; + bool directTested = false; + for (const bool direct : {false, true}) { + const std::string what = direct ? "GPU-mapped direct memory" : "watched memory"; + if (!AgcDriver::GuestMemory::WriteWatched()) { + std::cout << "no write watching: units stored next to a unit kept for the CPU in " << what << " not tested\n"; + continue; + } + if (direct && base.hostImportAlignment == 0) { + std::cout << "host imports unavailable: units stored next to a unit kept for the CPU in " << what << " not tested\n"; + continue; + } + struct Backing { + Recorder& recorder; + bool direct; + void* block = nullptr; + std::int64_t physical = -1; + ~Backing() { + try { + recorder.Sync(); + } catch (const std::exception& error) { + std::cerr << "(k) the recorded work did not finish, the test memory stays mapped: " << error.what() << '\n'; + return; + } + if (direct) { + if (block != nullptr) sceKernelMunmap(block, surfaceBytes); + if (physical >= 0) sceKernelReleaseDirectMemory(physical, surfaceBytes); + return; + } + if (block == nullptr) return; + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + ReleaseWatched(block, surfaceBytes); + } + } backing{recorder, direct}; + if (direct) { + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, surfaceBytes, unitBytes, 0, &backing.physical) == 0, "(k) allocate the direct memory"); + Require(sceKernelMapDirectMemory(&backing.block, surfaceBytes, 0x33, 0, backing.physical, unitBytes) == 0, "(k) map the direct memory for the GPU"); + } else { + backing.block = AllocateWatched(surfaceBytes, unitBytes); + Require(backing.block != nullptr, "(k) no watched block although write watching is on"); + GuestAllocations::Mutation mutation; + mutation.Add(backing.block, surfaceBytes, true, true, true); + } + auto* memory = static_cast<std::uint8_t*>(backing.block); + std::memset(memory, 0x5a, surfaceBytes); + const auto address = reinterpret_cast<std::uint64_t>(backing.block); + AgcDriver::GuestMemory::CollectWritesUncached(address, surfaceBytes); + TextureDetiler detiler(base); + auto context = base; + if (!direct) context.hostImportAlignment = 0; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = width; + resource.height = height; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kR64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = 56; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + Require(DescribeSurface(resource).guestBytes == surfaceBytes, "(k) unexpected surface size"); + if (direct && !AgcDriver::GuestMemory::Watched(address, surfaceBytes)) { + std::cout << "GPU-mapped direct memory is not write-watched here: units stored next to a unit kept for the CPU in " << what << " not tested\n"; + continue; + } + Require(AgcDriver::GuestMemory::Watched(address, surfaceBytes), "(k) " + what + " is not watched"); + if (direct && HostImportFor(context, address, surfaceBytes) == nullptr) { + std::cout << "host import of the direct memory refused: units stored next to a unit kept for the CPU in " << what << " not tested\n"; + continue; + } + Require(direct || HostImportFor(context, address, surfaceBytes) == nullptr, "(k) watched memory was imported although host imports are off for it"); + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const auto clear = [&](VkClearColorValue value) { + const auto commands = recorder.Commands(); + recorder.Keep(image); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function<PFN_vkCmdClearColorImage>("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &value, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + image->MarkDirty(); + }; + const auto store = [&] { + image->WriteBack(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + }; + const auto unitHolds = [&](std::size_t unit, const std::array<std::uint8_t, 4>& texel) { + std::vector<std::byte> read(unitBytes); + AgcDriver::GuestMemory::Read(address + unit * unitBytes, read); + for (std::size_t i = 0; i < unitBytes; ++i) { + if (std::to_integer<std::uint8_t>(read[i]) != texel[i % 4]) return false; + } + return true; + }; + clear({{1.0f, 0.0f, 0.0f, 1.0f}}); + memory[16] = 0x77; + AgcDriver::GuestMemory::CollectWritesUncached(address, surfaceBytes); + store(); + std::vector<std::byte> head(unitBytes); + AgcDriver::GuestMemory::Read(address, head); + Require(std::to_integer<std::uint8_t>(head[16]) == 0x77, "(k) " + what + ": the CPU store into unit 0 was overwritten by results it was newer than"); + for (std::size_t unit = 1; unit < surfaceBytes / unitBytes; ++unit) Require(unitHolds(unit, red), "(k) " + what + ": the first write-back did not store unit " + std::to_string(unit)); + clear({{0.0f, 0.0f, 1.0f, 1.0f}}); + store(); + for (std::size_t unit = 1; unit < surfaceBytes / unitBytes; ++unit) { + Require(unitHolds(unit, blue), "(k) " + what + ": unit " + std::to_string(unit) + " kept the results of the earlier write-back; that write-back's own store was taken for a CPU write because a neighbouring unit was kept for the CPU"); + } + AgcDriver::GuestMemory::Read(address, head); + Require(std::to_integer<std::uint8_t>(head[16]) == 0x77, "(k) " + what + ": the second write-back stored over unit 0, which the CPU wrote after that unit's generation"); + } + std::cout << "units stored next to a unit kept for the CPU in " << what << ": ok\n"; + if (direct) directTested = true; + } + return directTested; +} + void settledBaselineTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); #ifdef _WIN32 @@ -6379,6 +6510,9 @@ int main(int argc, char** argv) { unchangedCpuStampTests(device, recorder); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--kept-unit-settle-only") { + return keptUnitSettleTests(device, recorder) ? 0 : 77; + } if (argc == 2 && std::string_view(argv[1]) == "--integer-clear-only") { integerClearTests(device); return 0; @@ -6493,6 +6627,7 @@ int main(int argc, char** argv) { singlePassTests(device, recorder, false); singlePassTests(device, recorder, true); unchangedCpuStampTests(device, recorder); + keptUnitSettleTests(device, recorder); settledBaselineTests(device, recorder); importWatchTests(device); staleGenerationTests(device, recorder); From 7bfeeb9774abf1311fdbaace4b414cbb1e5a3be3 Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Tue, 13 Oct 2026 10:45:19 -0400 Subject: [PATCH 24/81] test(agc): skip the kept-unit settle direct case when its import is compared agc_storage_kept_unit_settle checked that the GPU-mapped direct memory was write-watched before it made the host import. On Linux, when host imports resolve write protection and dma-buf imports keep it, the host-pointer import is tried first in a window that does not stay watched, and dma-buf only when it fails (importAllocation in GuestBufferMemory.cpp). Where the host-pointer import of shared memory succeeds, as on Intel's ANV, the import takes the direct memory out of the write watch, so the case ran on the compared path: the upload saves the guest bytes, the first write-back keeps unit 0 by comparison and drops the saved bytes of every unit it selected, and the second write-back has nothing to compare. Before e5449936f it stored every unit whole, over the CPU's byte in unit 0, which is the failure reported on Linux; on main it throws (technical debt: results over memory outside the write watch without saved bytes). Neither depends on the settle change. On Windows, importing watched private memory (compared there) reproduces both: on 7e181fcc3, with or without this branch's Texture.cpp change, the second check fails with that message; on main the second write-back throws. The case now checks the write watch again after the import and returns 77, naming the reason, when the import took the memory out of it. Where the import keeps the memory watched (the Windows alias import, a Linux dma-buf import) it runs as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index beb762fe229..070b5f9b6cc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -4117,6 +4117,10 @@ bool keptUnitSettleTests(const Device& device, Recorder& recorder) { std::cout << "host import of the direct memory refused: units stored next to a unit kept for the CPU in " << what << " not tested\n"; continue; } + if (direct && !AgcDriver::GuestMemory::Watched(address, surfaceBytes)) { + std::cout << "the host import of the direct memory took it out of the write watch (its imports are compared): units stored next to a unit kept for the CPU in " << what << " not tested\n"; + continue; + } Require(direct || HostImportFor(context, address, surfaceBytes) == nullptr, "(k) watched memory was imported although host imports are off for it"); { auto image = StorageTexture::Create(context, detiler, resource, 0); From 7b6faf7dd29425817506584cc39f52433284af39 Mon Sep 17 00:00:00 2001 From: Evgeniy Kazarin <evgkaz0@gmail.com> Date: Tue, 13 Oct 2026 08:27:34 +0300 Subject: [PATCH 25/81] fix(build): preserve UTF-8 shader cache source paths --- CMakeLists.txt | 4 + .../recompiler/ShaderCacheVersion.cmake | 8 +- .../tests/test_shader_cache_native_paths.py | 78 +++++++++++++++++++ 3 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 core/shader/recompiler/tests/test_shader_cache_native_paths.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 8704dfdbd4e..4a84f355739 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,6 +78,10 @@ if(BUILD_TESTING) set_tests_properties(request_serializer PROPERTIES TIMEOUT 20) find_package(Python3 QUIET COMPONENTS Interpreter) if(Python3_Interpreter_FOUND) + add_test(NAME shader_cache_native_paths COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/test_shader_cache_native_paths.py + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ShaderCacheVersion.cmake ${CMAKE_COMMAND}) + set_tests_properties(shader_cache_native_paths PROPERTIES TIMEOUT 20) add_test(NAME pr_overlap_encoding COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_encoding.py) add_test(NAME pr_overlap_dependencies COMMAND ${Python3_EXECUTABLE} diff --git a/core/shader/recompiler/ShaderCacheVersion.cmake b/core/shader/recompiler/ShaderCacheVersion.cmake index e952236aef8..3216fd94ba7 100644 --- a/core/shader/recompiler/ShaderCacheVersion.cmake +++ b/core/shader/recompiler/ShaderCacheVersion.cmake @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.20) -file(STRINGS "${LIST_FILE}" entries) +file(STRINGS "${LIST_FILE}" entries ENCODING UTF-8) set(sources) set(guard) set(submodules) @@ -46,7 +46,7 @@ foreach(source IN LISTS sources) if(NOT source MATCHES "\\.(cpp|hpp|h)$") continue() endif() - file(STRINGS "${source}" lines REGEX "\"APS5_[A-Z0-9_]+\"") + file(STRINGS "${source}" lines ENCODING UTF-8 REGEX "\"APS5_[A-Z0-9_]+\"") foreach(line IN LISTS lines) string(REGEX MATCHALL "\"APS5_[A-Z0-9_]+\"" names "${line}") list(APPEND switches ${names}) @@ -56,7 +56,7 @@ foreach(source IN LISTS sources) continue() endif() get_filename_component(sourceDirectory "${source}" DIRECTORY) - file(STRINGS "${source}" lines REGEX "^[ \t]*#[ \t]*include[ \t]*\"[^\"]+\"") + file(STRINGS "${source}" lines ENCODING UTF-8 REGEX "^[ \t]*#[ \t]*include[ \t]*\"[^\"]+\"") foreach(line IN LISTS lines) string(REGEX REPLACE "^[ \t]*#[ \t]*include[ \t]*\"([^\"]+)\".*$" "\\1" included "${line}") set(resolved) @@ -109,7 +109,7 @@ foreach(source IN LISTS guard) continue() endif() file(RELATIVE_PATH relative "${ROOT}" "${source}") - file(STRINGS "${source}" lines REGEX "ShaderRecompiler|setenv|putenv") + file(STRINGS "${source}" lines ENCODING UTF-8 REGEX "ShaderRecompiler|setenv|putenv") foreach(line IN LISTS lines) if(line MATCHES "using[ \t]+namespace[ \t]+ShaderRecompiler" OR line MATCHES "namespace[ \t]+[A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*(::)?ShaderRecompiler[ \t]*;") list(APPEND failures "${relative} brings the ShaderRecompiler namespace in whole, which hides its calls from the shader disk cache key review: qualify them") diff --git a/core/shader/recompiler/tests/test_shader_cache_native_paths.py b/core/shader/recompiler/tests/test_shader_cache_native_paths.py new file mode 100644 index 00000000000..b2f1e8c57f3 --- /dev/null +++ b/core/shader/recompiler/tests/test_shader_cache_native_paths.py @@ -0,0 +1,78 @@ +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +SCRIPT = Path(sys.argv.pop(1)).resolve() +CMAKE = sys.argv.pop(1) + + +class ShaderCachePathTests(unittest.TestCase): + def fixture(self, root): + source = root / 'recompiler' / 'input.cpp' + source.parent.mkdir(parents=True) + source.write_text('const char* mode = "APS5_TEST_PATH";\n', encoding='utf-8') + listing = root / 'sources.txt' + listing.write_text(f'sources {source.as_posix()}\n', encoding='utf-8') + output = root / 'version.hpp' + return source, listing, output + + def run_script(self, root, listing, output): + return subprocess.run([CMAKE, f'-DROOT={root.as_posix()}', + f'-DRECOMPILER_DIR={(root / "recompiler").as_posix()}', + f'-DLIST_FILE={listing.as_posix()}', f'-DOUTPUT={output.as_posix()}', + '-P', str(SCRIPT)], capture_output=True, encoding='utf-8', errors='replace', timeout=10) + + def check_source_change(self, name): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) / name + source, listing, output = self.fixture(root) + result = self.run_script(root, listing, output) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + previous = output.read_bytes() + self.assertIn(b'APS5_TEST_PATH', previous) + source.write_text('const char* mode = "APS5_TEST_CHANGED";\n', encoding='utf-8') + result = self.run_script(root, listing, output) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertNotEqual(previous, output.read_bytes()) + self.assertIn(b'APS5_TEST_CHANGED', output.read_bytes()) + unchanged = output.stat().st_mtime_ns + result = self.run_script(root, listing, output) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(output.stat().st_mtime_ns, unchanged) + + def test_ascii(self): + self.check_source_change('plain path') + + def test_cyrillic(self): + self.check_source_change('\u0441\u0431\u043e\u0440\u043a\u0430 \u0438\u0433\u0440\u044b') + + def test_cjk(self): + self.check_source_change('\u6e2c\u8a66 path') + + def test_unicode_guard_rejects_unhashed_include(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) / '\u0441\u0431\u043e\u0440\u043a\u0430' + source, listing, output = self.fixture(root) + (root / 'shared.hpp').write_text('int example();\n', encoding='utf-8') + source.write_text('#include "../shared.hpp"\n', encoding='utf-8') + result = self.run_script(root, listing, output) + self.assertNotEqual(result.returncode, 0) + self.assertIn('does not hash', result.stdout + result.stderr) + self.assertFalse(output.exists()) + + def test_unicode_header_rejects_unhashed_include(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) / 'plain path' + source, listing, output = self.fixture(root) + name = '\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a.hpp' + (root / name).write_text('int example();\n', encoding='utf-8') + source.write_text(f'#include "../{name}"\n', encoding='utf-8') + result = self.run_script(root, listing, output) + self.assertNotEqual(result.returncode, 0) + self.assertIn('does not hash', result.stdout + result.stderr) + self.assertFalse(output.exists()) + + +unittest.main() From d50eed2df30ab58e6abf9dce2147bee022dadf56 Mon Sep 17 00:00:00 2001 From: krishalivaibhav <krishalivaibhav@gmail.com> Date: Tue, 13 Oct 2026 16:13:57 +0530 Subject: [PATCH 26/81] fix(memory): preserve process-owned services during exit --- .../libs/prx/libc/include/WindowsMappings.hpp | 4 +- core/libs/prx/libc/src/GuestAllocations.cpp | 4 +- core/libs/prx/libc/src/GuestArena.cpp | 4 +- core/libs/prx/libc/src/GuestWriteWatch.cpp | 8 +- core/libs/prx/libkernel/CMakeLists.txt | 10 ++ .../libkernel/DirectMemory/DirectMemory.cpp | 60 ++++++--- .../prx/libkernel/DirectMemory/MemoryPool.cpp | 4 +- core/libs/tests/GuestMemoryLifetime.cpp | 75 +++++++++++ core/libs/tests/GuestMemoryLifetimeModule.cpp | 126 ++++++++++++++++++ 9 files changed, 263 insertions(+), 32 deletions(-) create mode 100644 core/libs/tests/GuestMemoryLifetime.cpp create mode 100644 core/libs/tests/GuestMemoryLifetimeModule.cpp diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 87f9f783087..1af5d5a0bab 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -24,8 +24,8 @@ class WindowsMappings { enum class Collection { All, Armed, Fresh }; static WindowsMappings& Get() { - static WindowsMappings mappings; - return mappings; + static auto* mappings = new WindowsMappings; + return *mappings; } void* Reserve(void* address, std::size_t bytes) { diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index c7b286af879..910d6a8d885 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -41,8 +41,8 @@ struct Registry { }; Registry& registry() { - static Registry value; - return value; + static auto* value = new Registry; + return *value; } std::atomic<std::uint64_t> generation{1}; diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 5387ae03903..df6cdd71f08 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -49,8 +49,8 @@ std::uintptr_t alignUp(std::uintptr_t value, std::size_t alignment) { class Arena { public: static Arena& Get() { - static Arena arena; - return arena; + static auto* arena = new Arena; + return *arena; } bool Available() const { diff --git a/core/libs/prx/libc/src/GuestWriteWatch.cpp b/core/libs/prx/libc/src/GuestWriteWatch.cpp index 55f991f047e..959d940c38e 100644 --- a/core/libs/prx/libc/src/GuestWriteWatch.cpp +++ b/core/libs/prx/libc/src/GuestWriteWatch.cpp @@ -47,8 +47,8 @@ constexpr std::uintptr_t PageBytes = 4096; class Watch { public: static Watch& Get() { - static Watch watch; - return watch; + static auto* watch = new Watch; + return *watch; } bool Available() const { @@ -321,8 +321,8 @@ const bool g_opened = (Watch::Get(), true); class Watch { public: static Watch& Get() { - static Watch watch; - return watch; + static auto* watch = new Watch; + return *watch; } bool Available() const { diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 6d9e5fbdc67..44fe2d827f9 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -84,6 +84,16 @@ endif() configure_windows_unwind(libkernel) if(BUILD_TESTING) + if(NOT WIN32) + add_library(guest_memory_lifetime_module MODULE ${LIBS_INCLUDE_DIR}/tests/GuestMemoryLifetimeModule.cpp) + target_include_directories(guest_memory_lifetime_module PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_memory_lifetime_module PRIVATE libkernel libc) + add_test_executable(guest_memory_lifetime_tests ${LIBS_INCLUDE_DIR}/tests/GuestMemoryLifetime.cpp) + target_link_libraries(guest_memory_lifetime_tests PRIVATE ${CMAKE_DL_LIBS}) + add_dependencies(guest_memory_lifetime_tests guest_memory_lifetime_module) + add_test(NAME guest_memory_lifetime COMMAND guest_memory_lifetime_tests $<TARGET_FILE:guest_memory_lifetime_module>) + set_tests_properties(guest_memory_lifetime PROPERTIES TIMEOUT 25) + endif() add_test_executable(download_data_mount_tests ${LIBS_INCLUDE_DIR}/tests/GuestDownloadDataMount.cpp) target_include_directories(download_data_mount_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(download_data_mount_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index dc3a1bbcfdc..47931f4f230 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -241,19 +241,19 @@ int PhysicalFile() { return file; } -std::vector<bool> g_physicalUsed(DIRECT_MEMORY_SIZE / PS5_PAGE_SIZE); +std::vector<bool>& PhysicalUsed(); void ZeroReusedPhysical(std::uint64_t start, std::size_t bytes) { const auto first = start / PS5_PAGE_SIZE; const auto count = bytes / PS5_PAGE_SIZE; for (std::size_t page = 0; page < count;) { - if (!g_physicalUsed[first + page]) { - g_physicalUsed[first + page] = true; + if (!PhysicalUsed()[first + page]) { + PhysicalUsed()[first + page] = true; ++page; continue; } std::size_t run = 0; - while (page + run < count && g_physicalUsed[first + page + run]) ++run; + while (page + run < count && PhysicalUsed()[first + page + run]) ++run; const auto offset = (first + page) * PS5_PAGE_SIZE; void* view = ::mmap(nullptr, run * PS5_PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, PhysicalFile(), static_cast<off_t>(offset)); if (view == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map reused direct memory"); @@ -337,9 +337,6 @@ struct PhysicalPage { std::uint64_t offset; }; -std::mutex g_directLock; -std::map<std::uint64_t, PhysicalPage> g_physPages; - struct DirectMapping { std::uintptr_t end; std::uint64_t phys; @@ -347,13 +344,47 @@ struct DirectMapping { std::shared_ptr<PhysicalBacking> backing; }; -std::map<std::uintptr_t, DirectMapping> g_directMappings; struct PhysicalSpan { std::uint64_t end; std::vector<std::uintptr_t> bases; }; -std::map<std::uint64_t, PhysicalSpan> g_physicalSpans; +struct ProtectedRange { + std::uintptr_t end; + int prot; +}; + +struct DirectMemoryState { +#if defined(__linux__) + std::vector<bool> physicalUsed = std::vector<bool>(DIRECT_MEMORY_SIZE / PS5_PAGE_SIZE); +#endif + std::map<std::uint64_t, PhysicalSpan> physicalSpans; + std::mutex directLock; + std::map<std::uint64_t, PhysicalPage> physPages; + std::map<std::uintptr_t, DirectMapping> directMappings; + std::mutex protectionLock; + std::map<std::uintptr_t, ProtectedRange> protections; + std::mutex reservationLock; + std::map<std::uintptr_t, std::uintptr_t> reservations; +}; + +DirectMemoryState& MemoryState() { + static auto* state = new DirectMemoryState; + return *state; +} + +#if defined(__linux__) +std::vector<bool>& PhysicalUsed() { return MemoryState().physicalUsed; } +#endif + +auto& g_directLock = MemoryState().directLock; +auto& g_physPages = MemoryState().physPages; +auto& g_directMappings = MemoryState().directMappings; +auto& g_physicalSpans = MemoryState().physicalSpans; +auto& g_protectionLock = MemoryState().protectionLock; +auto& g_protections = MemoryState().protections; +auto& g_reservationLock = MemoryState().reservationLock; +auto& g_reservations = MemoryState().reservations; void SplitPhysicalSpan(std::uint64_t at) { auto span = g_physicalSpans.upper_bound(at); @@ -444,7 +475,6 @@ void ErasePhysMappings(std::uint64_t first, std::uint64_t last) { } } - void ValidatePhysicalRange(std::uint64_t phys, std::size_t len) { for (std::size_t offset = 0; offset < len; offset += PS5_PAGE_SIZE) { if (!g_physPages.contains(phys + offset)) { @@ -749,13 +779,6 @@ void* MapPlaced(void* addr, size_t len, int prot, int flags, size_t alignment) { return aligned; } -struct ProtectedRange { - std::uintptr_t end; - int prot; -}; -std::mutex g_protectionLock; -std::map<std::uintptr_t, ProtectedRange> g_protections; - void EraseProtections(std::uintptr_t start, std::uintptr_t end) { auto it = g_protections.lower_bound(start); if (it != g_protections.begin() && std::prev(it)->second.end > start) --it; @@ -768,9 +791,6 @@ void EraseProtections(std::uintptr_t start, std::uintptr_t end) { } } -std::mutex g_reservationLock; -std::map<std::uintptr_t, std::uintptr_t> g_reservations; - void EraseReservations(const void* addr, size_t len) { const auto start = reinterpret_cast<std::uintptr_t>(addr); const auto end = start + len; diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp index 8fa22b5fbc8..fc082fb8e95 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp @@ -52,8 +52,8 @@ bool PageSpan(uint64_t start, size_t len, uint64_t& lo, uint64_t& hi) { struct PhysicalMemoryPool { static PhysicalMemoryPool& Instance() { - static PhysicalMemoryPool inst; - return inst; + static auto* inst = new PhysicalMemoryPool; + return *inst; } int Alloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut) { diff --git a/core/libs/tests/GuestMemoryLifetime.cpp b/core/libs/tests/GuestMemoryLifetime.cpp new file mode 100644 index 00000000000..80a0dec2727 --- /dev/null +++ b/core/libs/tests/GuestMemoryLifetime.cpp @@ -0,0 +1,75 @@ +#include <cerrno> +#include <chrono> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <dlfcn.h> +#include <sys/wait.h> +#include <thread> +#include <unistd.h> +#include <signal.h> + +namespace { + +int fixtureDestroyed = 0; +using Exercise = int (*)(); +Exercise exercise = nullptr; + +void ProbeAfterStaticDestruction() { + if (fixtureDestroyed != 1 || exercise == nullptr) { + std::fputs("memory lifetime: fixture finalizer did not run before the exit probe\n", stderr); + std::_Exit(1); + } + if (exercise() != 0) std::_Exit(1); + std::fputs("memory lifetime: allocation, backing, mapping and explicit free survived native exit\n", stderr); +} + +int RunChild(const char* module) { + if (std::atexit(ProbeAfterStaticDestruction) != 0) return 2; + auto* handle = dlopen(module, RTLD_NOW | RTLD_LOCAL); + if (handle == nullptr) { + std::fprintf(stderr, "memory lifetime: load failed: %s\n", dlerror()); + std::_Exit(2); + } + const auto prepare = reinterpret_cast<int (*)(int*)>(dlsym(handle, "GuestMemoryLifetimePrepare")); + exercise = reinterpret_cast<Exercise>(dlsym(handle, "GuestMemoryLifetimeExercise")); + if (prepare == nullptr || exercise == nullptr || prepare(&fixtureDestroyed) != 0) std::_Exit(2); + std::exit(0); +} + +} + +int main(int argc, char** argv) { + if (argc == 3 && std::strcmp(argv[1], "--child") == 0) return RunChild(argv[2]); + if (argc != 2) return 2; + const auto child = fork(); + if (child == -1) return 2; + if (child == 0) { + execl(argv[0], argv[0], "--child", argv[1], static_cast<char*>(nullptr)); + std::_Exit(2); + } + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(20); + int status = 0; + for (;;) { + const auto result = waitpid(child, &status, WNOHANG); + if (result == child) break; + if (result < 0) { + if (errno == EINTR) continue; + kill(child, SIGKILL); + while (waitpid(child, &status, 0) < 0 && errno == EINTR) {} + return 2; + } + if (std::chrono::steady_clock::now() >= deadline) { + kill(child, SIGKILL); + while (waitpid(child, &status, 0) < 0 && errno == EINTR) {} + std::fputs("memory lifetime: exit probe timed out\n", stderr); + return 1; + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + std::fprintf(stderr, "memory lifetime: child failed during native exit (status=%d)\n", status); + return 1; + } + return 0; +} diff --git a/core/libs/tests/GuestMemoryLifetimeModule.cpp b/core/libs/tests/GuestMemoryLifetimeModule.cpp new file mode 100644 index 00000000000..173ac84e32e --- /dev/null +++ b/core/libs/tests/GuestMemoryLifetimeModule.cpp @@ -0,0 +1,126 @@ +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include <cstdio> +#include <cstring> +#include <exception> +#include <stdexcept> + +namespace { + +int* fixtureDestroyed = nullptr; +std::int64_t physical = -1; +void* mapping = nullptr; +bool watched = false; + +struct StaticExitMarker { + ~StaticExitMarker() { if (fixtureDestroyed != nullptr) *fixtureDestroyed = 1; } +} marker; + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void CheckPhysical(std::int64_t expected) { + std::int64_t start = -1, end = -1; + int type = -1; + Require(DirectMemoryFind(expected, false, &start, &end, &type), "physical pool lost a live allocation during exit"); + Require(start == expected && end == expected + static_cast<std::int64_t>(PS5_PAGE_SIZE) && type == 0, "physical pool range changed during exit"); +} + +void CheckMapping(void* address, std::int64_t expected) { + std::uintptr_t start = 0, end = 0; + std::uint64_t offset = 0; + int type = -1; + Require(QueryDirectMapping(reinterpret_cast<std::uintptr_t>(address), &start, &end, &offset, &type), "direct backing map lost a live allocation during exit"); + Require(start == reinterpret_cast<std::uintptr_t>(address) && end == start + PS5_PAGE_SIZE && offset == static_cast<std::uint64_t>(expected) && type == 0, "direct backing range changed during exit"); + GuestAllocations::Mutation mutation; + Require(mutation.Covers(address, PS5_PAGE_SIZE), "guest allocation registry lost a live mapping during exit"); + Require(mutation.Find(address).bytes == PS5_PAGE_SIZE, "guest allocation size changed during exit"); +} + +void CheckUnmapped(void* address) { + std::uintptr_t begin = 0, end = 0; + std::uint64_t offset = 0; + int type = -1; + Require(!QueryDirectMapping(reinterpret_cast<std::uintptr_t>(address), &begin, &end, &offset, &type), "explicit unmap did not remove the direct mapping"); + int protection = -1; + Require(!GuestProtection(reinterpret_cast<std::uintptr_t>(address), &protection), "explicit unmap did not remove protection metadata"); + GuestAllocations::Mutation mutation; + Require(!mutation.Overlaps(address, PS5_PAGE_SIZE), "explicit unmap did not remove the guest allocation"); +} + +void IgnoreWritten(void*, std::uintptr_t, std::uintptr_t) {} + +} + +extern "C" int GuestMemoryLifetimePrepare(int* destroyed) { + try { + fixtureDestroyed = destroyed; + Require(DirectMemoryAlloc(0, DIRECT_MEMORY_SIZE, PS5_PAGE_SIZE, PS5_PAGE_SIZE, 0, &physical) == 0, "prepare physical allocation failed"); + Require(DoMapDirect(&mapping, PS5_PAGE_SIZE, 3, 0, physical, PS5_PAGE_SIZE) == 0, "prepare direct mapping failed"); + CheckPhysical(physical); + CheckMapping(mapping, physical); + std::memset(mapping, 0x6d, PS5_PAGE_SIZE); + watched = GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix(); + if (watched) Require(GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(reinterpret_cast<std::uintptr_t>(mapping), PS5_PAGE_SIZE), "prepare write watch missing"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "memory lifetime prepare: %s\n", error.what()); + return 1; + } +} + +extern "C" int GuestMemoryLifetimeExercise() { + const char* stage = "physical allocation query"; + try { + CheckPhysical(physical); + stage = "backing mapping query"; + CheckMapping(mapping, physical); + Require(static_cast<unsigned char*>(mapping)[0] == 0x6d, "physical data changed during exit"); + stage = "write watch"; + if (watched) { + Require(GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(reinterpret_cast<std::uintptr_t>(mapping), PS5_PAGE_SIZE), "write watch lost a live mapping during exit"); + Require(GuestWriteWatch::GuestWriteWatchCollect_nid_postfix(reinterpret_cast<std::uintptr_t>(mapping), PS5_PAGE_SIZE, IgnoreWritten, nullptr), "write watch collection failed during exit"); + static_cast<unsigned char*>(mapping)[0] = 0x2a; + } + stage = "mapping protection"; + Require(DoMprotect(mapping, PS5_PAGE_SIZE, 1) == 0, "late read-only protection failed"); + int protection = -1; + Require(GuestProtection(reinterpret_cast<std::uintptr_t>(mapping), &protection) && protection == 1, "late protection registry failed"); + Require(DoMprotect(mapping, PS5_PAGE_SIZE, 3) == 0, "late read/write protection failed"); + stage = "explicit unmap and free"; + Require(DoMunmap(mapping, PS5_PAGE_SIZE) == 0, "late direct unmap failed"); + CheckUnmapped(mapping); + DirectMemoryFree(physical, PS5_PAGE_SIZE); + std::int64_t start = -1, end = -1; + int type = -1; + Require(!DirectMemoryFind(physical, false, &start, &end, &type), "explicit physical free did not remove the allocation"); + + stage = "physical offset reuse"; + std::int64_t reused = -1; + Require(DirectMemoryAlloc(physical, physical + PS5_PAGE_SIZE, PS5_PAGE_SIZE, PS5_PAGE_SIZE, 0, &reused) == 0 && reused == physical, "late physical allocation failed"); + mapping = nullptr; + Require(DoMapDirect(&mapping, PS5_PAGE_SIZE, 3, 0, reused, PS5_PAGE_SIZE) == 0, "late direct mapping failed"); + CheckPhysical(reused); + CheckMapping(mapping, reused); + const auto* bytes = static_cast<const unsigned char*>(mapping); + for (std::size_t i = 0; i < PS5_PAGE_SIZE; ++i) Require(bytes[i] == 0, "reused physical backing was not zeroed"); + std::memset(mapping, 0x17, PS5_PAGE_SIZE); + Require(DoMunmap(mapping, PS5_PAGE_SIZE) == 0, "late reused mapping unmap failed"); + CheckUnmapped(mapping); + DirectMemoryFree(reused, PS5_PAGE_SIZE); + + stage = "late arena reservation"; + void* reserved = nullptr; + Require(DoReserveVirtual(&reserved, PS5_PAGE_SIZE, 0, PS5_PAGE_SIZE) == 0, "late arena reservation failed"); + std::uintptr_t begin = 0, finish = 0; + Require(GuestReservation(reinterpret_cast<std::uintptr_t>(reserved), &begin, &finish) && begin == reinterpret_cast<std::uintptr_t>(reserved) && finish == begin + PS5_PAGE_SIZE, "late reservation registry failed"); + Require(DoMunmap(reserved, PS5_PAGE_SIZE) == 0, "late reserved mapping unmap failed"); + Require(!GuestReservation(reinterpret_cast<std::uintptr_t>(reserved), &begin, &finish), "explicit unmap did not remove the reservation"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "memory lifetime exit probe (%s): %s\n", stage, error.what()); + return 1; + } +} From 39317d5b31d58bc0c92c22593b8bf90cfd38a1bf Mon Sep 17 00:00:00 2001 From: Rujul Talekar <113801876+Roojool@users.noreply.github.com> Date: Tue, 13 Oct 2026 02:54:36 +0530 Subject: [PATCH 27/81] fix(tools): decode Ninja targets using its build-file encoding --- tools/check_test_registration.py | 11 +++++++- tools/tests/test_test_registration.py | 40 +++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index 7b00d94d3e1..fba95ce4768 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -1,6 +1,7 @@ import argparse import re import subprocess +import sys from pathlib import Path TEST_OUTPUT = r"tests[\\/]" @@ -21,8 +22,16 @@ def built(build): raise ValueError(f"test registration audit requires a Ninja generator, got {generator!r}") if not program or program.endswith("-NOTFOUND"): raise FileNotFoundError(f"CMAKE_MAKE_PROGRAM is not set in {Path(build) / 'CMakeCache.txt'}") + encoding = "utf-8" + if sys.platform == "win32": + codepage = subprocess.run([program, "-t", "wincodepage"], capture_output=True, text=True, + encoding="ascii", check=True).stdout.strip() + encodings = {"Build file encoding: UTF-8": "utf-8", "Build file encoding: ANSI": "mbcs"} + if codepage not in encodings: + raise ValueError(f"unsupported Ninja build-file encoding: {codepage!r}") + encoding = encodings[codepage] listed = subprocess.run([program, "-C", str(build), "-t", "targets", "all"], - capture_output=True, text=True, check=True).stdout + capture_output=True, text=True, encoding=encoding, check=True).stdout names = set() for line in listed.splitlines(): path, _, rule = line.partition(": ") diff --git a/tools/tests/test_test_registration.py b/tools/tests/test_test_registration.py index e9862c2bc3d..1b201adbf2e 100644 --- a/tools/tests/test_test_registration.py +++ b/tools/tests/test_test_registration.py @@ -41,7 +41,8 @@ def test_hyphenated_executable_is_discovered(self): with tempfile.TemporaryDirectory() as directory: build = Path(directory) (build / "CMakeCache.txt").write_text("CMAKE_MAKE_PROGRAM:FILEPATH=/configured/ninja\nCMAKE_GENERATOR:INTERNAL=Ninja\n", encoding="utf-8") - with patch.object(check_test_registration.subprocess, "run", return_value=SimpleNamespace(stdout=output)) as run: + with patch.object(check_test_registration.subprocess, "run", side_effect=lambda command, **kwargs: SimpleNamespace( + stdout="Build file encoding: UTF-8\n" if command[-1] == "wincodepage" else output)) as run: self.assertEqual(check_test_registration.built(build), {"buffer-bounds_tests"}) self.assertEqual(run.call_args[0][0][0], "/configured/ninja") @@ -50,7 +51,8 @@ def test_nested_test_output_is_discovered(self): with tempfile.TemporaryDirectory() as directory: build = Path(directory) (build / "CMakeCache.txt").write_text("CMAKE_MAKE_PROGRAM:FILEPATH=/configured/ninja\nCMAKE_GENERATOR:INTERNAL=Ninja Multi-Config\n", encoding="utf-8") - with patch.object(check_test_registration.subprocess, "run", return_value=SimpleNamespace(stdout=output)) as run: + with patch.object(check_test_registration.subprocess, "run", side_effect=lambda command, **kwargs: SimpleNamespace( + stdout="Build file encoding: UTF-8\n" if command[-1] == "wincodepage" else output)) as run: self.assertEqual(check_test_registration.built(build), {"buffer_bounds_tests"}) self.assertEqual(run.call_args[0][0][0], "/configured/ninja") @@ -151,5 +153,39 @@ def test_missing_make_program_never_runs_build_tool(self): check_test_registration.built(build) run.assert_not_called() + @unittest.skipIf(shutil.which("ninja") is None, "requires a local Ninja to exercise its real output encoding") + def test_ninja_output_encoding_with_a_cp1252_default(self): + program = shutil.which("ninja") + actual_run = check_test_registration.subprocess.run + for encoding in (("utf-8", "mbcs") if sys.platform == "win32" else ("utf-8",)): + with self.subTest(encoding=encoding), tempfile.TemporaryDirectory() as directory: + build = Path(directory) + name = "caf\u00e9_tests".encode(encoding, errors="ignore").decode(encoding) + (build / "CMakeCache.txt").write_text( + f"CMAKE_MAKE_PROGRAM:FILEPATH={program}\nCMAKE_GENERATOR:INTERNAL=Ninja\n", encoding="utf-8") + (build / "build.ninja").write_text( + "rule CXX_EXECUTABLE_LINKER\n command = true\n" + f"build tests/{name}: CXX_EXECUTABLE_LINKER\n", encoding=encoding) + + def run(command, **kwargs): + if command[-1] == "wincodepage": + return SimpleNamespace(stdout="Build file encoding: " + ("ANSI" if encoding == "mbcs" else "UTF-8")) + kwargs.setdefault("encoding", "cp1252") + return actual_run(command, **kwargs) + + with patch.object(check_test_registration.subprocess, "run", side_effect=run): + self.assertEqual(check_test_registration.built(build), {name}) + + def test_unknown_windows_ninja_encoding_is_rejected(self): + with tempfile.TemporaryDirectory() as directory: + build = Path(directory) + (build / "CMakeCache.txt").write_text( + "CMAKE_MAKE_PROGRAM:FILEPATH=/configured/ninja\nCMAKE_GENERATOR:INTERNAL=Ninja\n", encoding="utf-8") + with patch.object(check_test_registration.sys, "platform", "win32"), patch.object( + check_test_registration.subprocess, "run", return_value=SimpleNamespace(stdout="unknown")) as run: + with self.assertRaisesRegex(ValueError, "unsupported Ninja build-file encoding"): + check_test_registration.built(build) + run.assert_called_once() + if __name__ == "__main__": unittest.main() From 69375305f500004f5648e683eb012323e6e9ad6b Mon Sep 17 00:00:00 2001 From: ArjenS-LA <arjensingh@outlook.com> Date: Tue, 13 Oct 2026 23:59:02 -0700 Subject: [PATCH 28/81] fix(relinker): reject collisions between generated outputs --- core/relinker/CMakeLists.txt | 4 + core/relinker/main.cpp | 113 +++++++++++++ .../relinker/tests/test_output_collisions.py | 153 ++++++++++++++++++ .../tests/test_secondary_output_collisions.py | 101 ++++++++++++ 4 files changed, 371 insertions(+) create mode 100644 core/relinker/relinker/tests/test_output_collisions.py create mode 100644 core/relinker/relinker/tests/test_secondary_output_collisions.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 2a01d4bee20..97332a6ba09 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -547,6 +547,10 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_magic.py $<TARGET_FILE:relinker>) add_test(NAME input_preservation COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_preservation.py $<TARGET_FILE:relinker>) + add_test(NAME output_collisions COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_output_collisions.py $<TARGET_FILE:relinker>) + add_test(NAME secondary_output_collisions COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_secondary_output_collisions.py $<TARGET_FILE:relinker>) add_test(NAME input_file COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_file.py $<TARGET_FILE:relinker>) add_test(NAME null_symbol_relocations COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index cf63c3296e5..f89537ab6dd 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -14,6 +14,7 @@ #include <filesystem> #include <iostream> #include <memory> +#include <random> #include <string> #include <vector> @@ -68,6 +69,100 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, return {inputPath, absPath, std::move(converted.Analysis), std::move(converted.Modules), std::move(converted.Executable), std::move(converted.WindowsDiagnostics)}; } +#ifdef _WIN32 + +class OutputIdentityDirectory { +public: + OutputIdentityDirectory(const OutputIdentityDirectory&) = delete; + OutputIdentityDirectory& operator=(const OutputIdentityDirectory&) = delete; + + explicit OutputIdentityDirectory(const std::filesystem::path& parent) { + std::random_device random; + for (unsigned attempt = 0; attempt < 64; ++attempt) { + const auto candidate = parent / (".anyps5-output-identity-" + std::to_string(random()) + "-" + std::to_string(random())); + std::error_code error; + if (std::filesystem::create_directory(candidate, error)) { + directory = candidate; + return; + } + if (error && error != std::errc::file_exists) + throw std::filesystem::filesystem_error("Cannot create output identity directory", candidate, error); + } + throw Domain::RelinkerException("Cannot reserve an output identity directory in " + Io::Utf8Path(parent)); + } + + ~OutputIdentityDirectory() { + if (directory.empty()) return; + try { + Remove(); + } catch (const std::exception& error) { + std::cerr << "FAIL: Output identity cleanup: " << error.what() << '\n'; + } + } + + std::filesystem::path GetPath() const { return directory / "file"; } + const std::filesystem::path& GetDirectory() const { return directory; } + + void Remove() { + std::filesystem::remove(GetPath()); + std::filesystem::remove(directory); + directory.clear(); + } + +private: + std::filesystem::path directory; +}; + +#endif + +bool SameOutputFile(const std::filesystem::path& first, const std::filesystem::path& second) { + if (std::filesystem::equivalent(first, second)) return true; +#ifdef _WIN32 + if (std::filesystem::status(first).permissions() != std::filesystem::status(second).permissions() && + std::filesystem::hard_link_count(first) > 1 && std::filesystem::hard_link_count(second) > 1) { + for (const auto& path : {first, second}) { + if ((std::filesystem::status(path).permissions() & std::filesystem::perms::owner_write) == std::filesystem::perms::none) + throw Domain::RelinkerException("Cannot compare read-only output identities: " + Io::Utf8Path(path)); + } + OutputIdentityDirectory firstDirectory(first.parent_path()); + OutputIdentityDirectory secondDirectory(second.parent_path()); + std::filesystem::create_hard_link(first, firstDirectory.GetPath()); + std::filesystem::create_hard_link(second, secondDirectory.GetPath()); + const bool same = std::filesystem::equivalent(firstDirectory.GetPath(), secondDirectory.GetPath()); + secondDirectory.Remove(); + firstDirectory.Remove(); + return same; + } +#endif + return false; +} + +bool SameOutputDestination(const std::filesystem::path& first, const std::filesystem::path& second) { + const auto firstPath = std::filesystem::weakly_canonical(first); + const auto secondPath = std::filesystem::weakly_canonical(second); + if (firstPath == secondPath) return true; + if (std::filesystem::exists(first) && std::filesystem::exists(second)) return SameOutputFile(first, second); +#ifdef _WIN32 + auto firstName = Io::Utf8Path(firstPath.filename()); + auto secondName = Io::Utf8Path(secondPath.filename()); + for (auto* name : {&firstName, &secondName}) + for (auto& character : *name) + if (character >= 'A' && character <= 'Z') character = static_cast<char>(character + ('a' - 'A')); + if (firstName == secondName && std::filesystem::exists(firstPath.parent_path()) && + std::filesystem::exists(secondPath.parent_path()) && std::filesystem::equivalent(firstPath.parent_path(), secondPath.parent_path())) { + OutputIdentityDirectory probe(firstPath.parent_path()); + auto alternateName = Io::Utf8Path(probe.GetDirectory().filename()); + for (auto& character : alternateName) + if (character >= 'a' && character <= 'z') character = static_cast<char>(character - ('a' - 'A')); + const auto alternate = firstPath.parent_path() / Io::NativePath(alternateName); + const bool same = std::filesystem::exists(alternate) && std::filesystem::equivalent(probe.GetDirectory(), alternate); + probe.Remove(); + return same; + } +#endif + return false; +} + int Run(const int argc, char* argv[]) { Cli::Args args; try { @@ -183,6 +278,24 @@ int Run(const int argc, char* argv[]) { checkOutputInputs(Io::NativePath(absPath), "Executable"); for (const auto& artifact : guestArtifacts) checkOutputInputs(artifact.Path, "Guest module"); + std::vector<std::filesystem::path> outputPaths; + for (const auto& executable : executables) outputPaths.push_back(Io::NativePath(executable.OutputPath)); + for (const auto& artifact : guestArtifacts) outputPaths.push_back(artifact.Path); + outputPaths.insert(outputPaths.end(), registryPaths.begin(), registryPaths.end()); + if (desktopEntry) { + outputPaths.push_back(desktopEntry->IconPath); + outputPaths.push_back(desktopEntry->EntryPath); + } + if (args.windowsDiagnostics) outputPaths.push_back(std::filesystem::absolute("windows-diagnostics-imports.txt")); + for (std::size_t index = 0; index < outputPaths.size(); ++index) { + for (std::size_t previous = 0; previous < index; ++previous) { + const auto& first = outputPaths[index]; + const auto& second = outputPaths[previous]; + if (SameOutputDestination(first, second)) + throw Domain::RelinkerException("Output paths collide: " + Io::Utf8Path(first) + " and " + Io::Utf8Path(second)); + } + } + if (args.windowsDiagnostics) fileWriter.Write("windows-diagnostics-imports.txt", executables.front().Diagnostics); const auto outputCount = guestArtifacts.size() + executables.size(); progress.Report({Domain::ProgressStage::Write, absPath, 0, outputCount}); diff --git a/core/relinker/relinker/tests/test_output_collisions.py b/core/relinker/relinker/tests/test_output_collisions.py new file mode 100644 index 00000000000..2026412310a --- /dev/null +++ b/core/relinker/relinker/tests/test_output_collisions.py @@ -0,0 +1,153 @@ +import itertools +import json +import os +from pathlib import Path +import stat +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import PLAIN_SITE, guest_fixture, main_fixture +from test_linux_desktop_entry_output import png + + +def prepare(case, windows): + case.mkdir() + source = case / "input.elf" + source.write_bytes(main_fixture()) + modules = case / "sce_module" + modules.mkdir() + module = modules / "sample.prx" + module.write_bytes(guest_fixture(PLAIN_SITE)) + output = case / ("output.exe" if windows else "output.elf") + paths = { + "executable": output, + "module": case / "app0/sce_module/sample.prx.guest.prx", + "registry": case / "output.registry.json", + "module-registry": case / "output.sample.prx.guest.prx.registry.json", + } + if windows: + paths["diagnostics"] = case / "windows-diagnostics-imports.txt" + else: + (case / "sce_sys").mkdir() + (case / "sce_sys/icon0.png").write_bytes(png(1, 1)) + paths["icon"] = Path(f"{output}.png") + paths["desktop"] = Path(f"{output}.desktop") + for path in paths.values(): + path.parent.mkdir(parents=True, exist_ok=True) + options = ["--windows", "--windows-diagnostics"] if windows else [] + command = [*options, "--registry", str(source), str(output)] + return command, paths, {source: source.read_bytes(), module: module.read_bytes()} + + +def case_sensitive(directory): + probe = directory / "CaseProbe" + probe.write_bytes(b"probe") + sensitive = not (directory / "caseprobe").exists() + probe.unlink() + return sensitive + + +def main(): + relinker = Path(sys.argv[1]).resolve() + checked = 0 + with tempfile.TemporaryDirectory(prefix="anyps5-output-collisions-") as directory: + work = Path(directory) + for windows in (False, True): + command, paths, inputs = prepare(work / f"{windows}-control", windows) + links = paths["executable"].parent / "separate-links" + links.mkdir() + for name, path in paths.items(): + path.write_bytes(b"previous output") + os.link(path, links / name) + result = subprocess.run([str(relinker), *command], cwd=paths["executable"].parent, + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (result.stdout, result.stderr) + for name in ("executable", "module"): + assert paths[name].read_bytes().startswith(b"MZ" if windows else b"\x7fELF") + for name in ("registry", "module-registry"): + json.loads(paths[name].read_text()) + for path, original in inputs.items(): + assert path.read_bytes() == original + assert not list(paths["executable"].parent.rglob(".anyps5-output-identity-*")) + for first, second in itertools.combinations(paths, 2): + case = work / f"{windows}-{first}-{second}" + command, outputs, inputs = prepare(case, windows) + previous = b"previous output" + outputs[first].write_bytes(previous) + os.link(outputs[first], outputs[second]) + for name, path in outputs.items(): + if name not in (first, second): + path.write_bytes(previous) + result = subprocess.run([str(relinker), *command], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "Output paths collide" in result.stderr, ( + windows, first, second, result.returncode, result.stdout, result.stderr) + for path in outputs.values(): + assert path.read_bytes() == previous, path + for path, original in inputs.items(): + assert path.read_bytes() == original, path + assert not list(case.rglob(".anyps5-output-identity-*")) + checked += 1 + names = ["windows-diagnostics-imports.txt"] + if os.name == "nt" and not case_sensitive(work): + names.append("WINDOWS-DIAGNOSTICS-IMPORTS.TXT") + for index, name in enumerate(names): + for existing in (False, True): + case = work / f"diagnostics-executable-{index}-{existing}" + case.mkdir() + source = case / "input.elf" + source.write_bytes(main_fixture()) + output = case / name + if existing: + output.write_bytes(b"previous output") + result = subprocess.run([str(relinker), "--windows", "--windows-diagnostics", "--skip-sce-module", + str(source), str(output)], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "Output paths collide" in result.stderr, (result.stdout, result.stderr) + assert output.read_bytes() == b"previous output" if existing else not output.exists() + assert source.read_bytes() == main_fixture() + assert not list(case.rglob(".anyps5-output-identity-*")) + for existing in (False, True): + case = work / f"case-sensitive-{existing}" + case.mkdir() + if not case_sensitive(case): + print("Distinct case-variant outputs skipped: case-sensitive directory unavailable") + break + source = case / "input.elf" + source.write_bytes(main_fixture()) + output = case / "WINDOWS-DIAGNOSTICS-IMPORTS.TXT" + diagnostics = case / "windows-diagnostics-imports.txt" + if existing: + output.write_bytes(b"previous executable") + diagnostics.write_bytes(b"previous diagnostics") + result = subprocess.run([str(relinker), "--windows", "--windows-diagnostics", "--skip-sce-module", + str(source), str(output)], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (result.stdout, result.stderr) + assert output.read_bytes().startswith(b"MZ") and diagnostics.exists() + assert not os.path.samefile(output, diagnostics) + assert not list(case.rglob(".anyps5-output-identity-*")) + if os.name == "nt": + case = work / "readonly-identity" + command, paths, inputs = prepare(case, True) + output = paths["executable"] + output.write_bytes(b"previous output") + os.link(output, paths["module"]) + output.chmod(stat.S_IREAD) + try: + result = subprocess.run([str(relinker), *command], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "Cannot compare read-only output identities" in result.stderr, ( + result.stdout, result.stderr) + assert output.read_bytes() == b"previous output" + assert paths["module"].read_bytes() == b"previous output" + assert not output.stat().st_mode & stat.S_IWRITE + assert not list(case.rglob(".anyps5-output-identity-*")) + finally: + output.chmod(stat.S_IWRITE) + print(f"Output collision tests passed: {checked} hard-link pairs, direct paths, and identity controls") + + +if __name__ == "__main__": + main() diff --git a/core/relinker/relinker/tests/test_secondary_output_collisions.py b/core/relinker/relinker/tests/test_secondary_output_collisions.py new file mode 100644 index 00000000000..a0db558788b --- /dev/null +++ b/core/relinker/relinker/tests/test_secondary_output_collisions.py @@ -0,0 +1,101 @@ +import itertools +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import main_fixture +from test_output_collisions import prepare + + +def prepare_secondary(case, windows): + command, paths, inputs = prepare(case, windows) + options = [] + for name in ("secondary-one", "secondary-two"): + source = case / f"{name}.bin" + source.write_bytes(main_fixture()) + inputs[source] = source.read_bytes() + options.extend(["--executable", source.name]) + paths[name] = case / f"{source.name}{paths['executable'].suffix}" + paths[f"{name}-registry"] = case / f"{source.name}.registry.json" + if not windows: + icon = case / "sce_sys/icon0.png" + inputs[icon] = icon.read_bytes() + return [*command[:-2], *options, *command[-2:]], paths, inputs + + +def main(): + relinker = Path(sys.argv[1]).resolve() + secondary = {"secondary-one", "secondary-two"} + checked = 0 + with tempfile.TemporaryDirectory(prefix="anyps5-secondary-collisions-") as directory: + work = Path(directory) + for windows in (False, True): + case = work / f"{windows}-control" + command, paths, inputs = prepare_secondary(case, windows) + links = case / "separate-links" + links.mkdir() + for name, path in paths.items(): + path.write_bytes(b"previous output") + os.link(path, links / name) + result = subprocess.run([str(relinker), *command], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (result.stdout, result.stderr) + for name in ("executable", "module", *sorted(secondary)): + assert paths[name].read_bytes().startswith(b"MZ" if windows else b"\x7fELF"), paths[name] + for name, path in paths.items(): + if "registry" in name: + json.loads(path.read_text()) + for path, original in inputs.items(): + assert path.read_bytes() == original, path + assert not list(case.rglob(".anyps5-output-identity-*")) + + for first, second in itertools.combinations(paths, 2): + if not secondary.intersection((first, second)): + continue + case = work / f"{windows}-{first}-{second}" + command, outputs, inputs = prepare_secondary(case, windows) + previous = b"previous output" + outputs[first].write_bytes(previous) + os.link(outputs[first], outputs[second]) + for name, path in outputs.items(): + if name not in (first, second): + path.write_bytes(previous) + result = subprocess.run([str(relinker), *command], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "Output paths collide" in result.stderr, ( + windows, first, second, result.returncode, result.stdout, result.stderr) + for path in outputs.values(): + assert path.read_bytes() == previous, path + for path, original in inputs.items(): + assert path.read_bytes() == original, path + assert not list(case.rglob(".anyps5-output-identity-*")) + checked += 1 + + for existing in (False, True): + case = work / f"diagnostics-secondary-{existing}" + command, paths, inputs = prepare(case, True) + source = case / "windows-diagnostics-imports" + source.write_bytes(main_fixture()) + inputs[source] = source.read_bytes() + paths["executable"] = case / "output.txt" + paths["secondary-registry"] = case / "windows-diagnostics-imports.registry.json" + if existing: + for path in paths.values(): + path.write_bytes(b"previous output") + result = subprocess.run([str(relinker), *command[:-2], "--executable", source.name, + command[-2], str(paths["executable"])], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "Output paths collide" in result.stderr, (result.stdout, result.stderr) + for path in paths.values(): + assert path.read_bytes() == b"previous output" if existing else not path.exists(), path + for path, original in inputs.items(): + assert path.read_bytes() == original, path + assert not list(case.rglob(".anyps5-output-identity-*")) + print(f"Secondary output collision tests passed: {checked} hard-link pairs, direct paths, and identity controls") + + +if __name__ == "__main__": + main() From 9d412df7b598ac9750ed93993045abe7065b7ef1 Mon Sep 17 00:00:00 2001 From: mazen <mazenshaar@hotmail.com> Date: Tue, 13 Oct 2026 17:16:18 +0400 Subject: [PATCH 29/81] fix(agc): fall back out-of-range sceAgcInit versions to table 13 Pointer-shaped NID kW3GLb7QfPg calls from public homebrew succeed on console; returning an error left those titles failing init after #4835. --- core/libs/prx/libSceAgc/DcbState/src/Init.cpp | 26 ++++++++++++------- core/libs/prx/libSceAgc/tests/Commands.cpp | 10 +++---- docs/dev/debt/libSceAgc.md | 2 +- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceAgc/DcbState/src/Init.cpp b/core/libs/prx/libSceAgc/DcbState/src/Init.cpp index 27f4ec9738b..3d3d07f3c95 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Init.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Init.cpp @@ -7,21 +7,29 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -static constexpr int SCE_AGC_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80890001); +namespace { -extern "C" { +constexpr std::uint32_t kRegisterDefaultsVersionCount = 14; +constexpr std::uint32_t kRegisterDefaultsFallbackVersion = kRegisterDefaultsVersionCount - 1; + +std::uint32_t ResolveRegisterDefaultsVersion(std::uint32_t version) { + return version < kRegisterDefaultsVersionCount ? version : kRegisterDefaultsFallbackVersion; +} -int APS5_VABI sceAgcInit_0090(std::uint32_t* state, std::uint32_t version) { - Agc::Command::CheckAddress(reinterpret_cast<std::uintptr_t>(state), alignof(std::uint32_t), __func__); - Agc::Command::Require(version < 14, __func__, "unsupported AGC initialization version"); - return 0; } +extern "C" { + int APS5_VABI sceAgcInit(std::uint32_t version) { - if (version >= 14) return SCE_AGC_ERROR_INVALID_ARGUMENT; - Agc::Command::GetRegisterDefaults(version, false, __func__); - Agc::Command::GetRegisterDefaults(version, true, __func__); + const std::uint32_t defaultsVersion = ResolveRegisterDefaultsVersion(version); + Agc::Command::GetRegisterDefaults(defaultsVersion, false, __func__); + Agc::Command::GetRegisterDefaults(defaultsVersion, true, __func__); return 0; } +int APS5_VABI sceAgcInit_0090(std::uint32_t* state, std::uint32_t version) { + Agc::Command::CheckAddress(reinterpret_cast<std::uintptr_t>(state), alignof(std::uint32_t), __func__); + return sceAgcInit(version); +} + } diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 6fc0d679e7e..82736484e6b 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -52,8 +52,6 @@ extern "C" std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, namespace { -constexpr int SCE_AGC_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80890001); - void check(bool condition, const char* message) { if (!condition) { throw std::runtime_error(message); @@ -624,12 +622,12 @@ void testDefaults() { check(sceAgcInit_0090(&state, 8) == 0 && state == 0x12345678, "AGC initialization failed or modified caller state"); check(sceAgcInit_0090(&state, 13) == 0 && state == 0x12345678, "AGC version 13 initialization changed caller state"); expectFailure([] { sceAgcInit_0090(nullptr, 8); }); - expectFailure([&] { sceAgcInit_0090(&state, 14); }); + check(sceAgcInit_0090(&state, 14) == 0 && state == 0x12345678, "out-of-range AGC initialization version changed caller state"); check(sceAgcInit(8) == 0, "AGC version initialization failed"); check(sceAgcInit(13) == 0, "AGC version 13 initialization failed"); - check(sceAgcInit(14) == SCE_AGC_ERROR_INVALID_ARGUMENT, "unsupported AGC version did not return invalid argument"); - check(sceAgcInit(0xffffffffu) == SCE_AGC_ERROR_INVALID_ARGUMENT, "maximum AGC version did not return invalid argument"); - check(sceAgcInit(0xffffc278u) == SCE_AGC_ERROR_INVALID_ARGUMENT, "pointer-valued AGC argument did not return invalid argument"); + check(sceAgcInit(14) == 0, "out-of-range AGC version did not fall back"); + check(sceAgcInit(0xffffffffu) == 0, "maximum AGC version did not fall back"); + check(sceAgcInit(0xffffc278u) == 0, "pointer-valued AGC argument did not fall back"); for (std::uint32_t version = 0; version < 14; ++version) { for (const bool internal : {false, true}) { auto* first = Agc::Command::GetRegisterDefaults(version, internal, __func__); diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 2a413608079..b798ad659ea 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -7,7 +7,7 @@ - [sceAgcDcbSetCfRegisterDirect, sceAgcDcbSetCfRegisterRangeDirect](../../../core/libs/prx/libSceAgc/DcbState/src/Registers.cpp) (libSceAgc) - signatures follow [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Agc/SceAgc.cs), which agree; the SET_CONFIG_REG (0x68) packet is OpenAGC's. The driver does not execute SET_CONFIG_REG, so a submitted packet throws - [Command buffer cursors](../../../core/libs/prx/libSceAgc/Command/src/Packet.cpp) (libSceAgc) - a null `cursor_down` is taken as a buffer without downward allocations, whose limit is `top`. PPSA16064 builds one-packet buffers of 8 dwords that way; the console's handling is unconfirmed - [sceAgcGetRegisterDefaultsInternal](../../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - returns the version 0 internal table, as `sceAgcGetRegisterDefaults` returns the version 0 public one; [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns the table of the version passed to `sceAgcInit` instead, and null before it -- [sceAgcInit](../../../core/libs/prx/libSceAgc/DcbState/src/Init.cpp) (libSceAgc) - unsupported register-default versions return `0x80890001` following [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c); AnyPS5 keeps version 13 support because its register-default tables include it, while that reference accepts only versions 0–12. The upper version accepted by PS5 firmware is unverified +- [sceAgcInit](../../../core/libs/prx/libSceAgc/DcbState/src/Init.cpp) (libSceAgc) - versions 0–13 select their register-default tables; a version of 14 or above falls back to the version 13 tables and returns 0. [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns `0x80890001` above 12 for `sceAgcInit` and falls back to version 11 in `sceAgcGetRegisterDefaults2`. Public homebrew that imports NID `kW3GLb7QfPg` with a pointer-shaped first argument (PPSA99011, PPSA99995, [ps5-vulkan](https://github.com/mpereiraesaa/ps5-vulkan), [prospero-win](https://github.com/mpereiraesaa/prospero-win)) reports success on firmware 12.02 for that call shape; which table the firmware selects and the true upper version it accepts are unverified. `sceAgcInit_0090` (NID `23LRUSvYu1M`) checks the state pointer then follows `sceAgcInit`, as OpenAGC does - [sceAgcSetSubmitMode](../../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted - [sceAgcGetGsOversubscription](../../../core/libs/prx/libSceAgc/Shader/src/GsOversubscription.cpp) (libSceAgc) - signature and occupancy formula (base and expanded vertex/export capacities, budget shift, `GE_PC_ALLOC` / `SPI_SHADER_PGM_RSRC4_GS` encoding) follow KytyPS5 only, not confirmed on hardware or a title; a negative or NaN factor that gives a negative target, a missing GS context register or a zero `GE_MAX_OUTPUT_PER_SUBGROUP` throws - [sceAgcGetGsPrimPayload](../../../core/libs/prx/libSceAgc/Shader/src/GsPrimPayload.cpp) (libSceAgc) - the search does not stop at an `SPI_SHADER_IDX_FORMAT` (0x1C2) entry whose format is not `SPI_SHADER_2COMP`, as the analysis of the library declared in the pull request describes (its origin and firmware version are not identified), while [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1285-L1310) stops there; not verified on PS5 hardware. A null payload, a null shader or a register count with a null register array throws instead of faulting on the memory access From 4832bd825cd9cf37b7960d26fcd607540bd670c5 Mon Sep 17 00:00:00 2001 From: mazen <mazenshaar@hotmail.com> Date: Tue, 13 Oct 2026 17:22:40 +0400 Subject: [PATCH 30/81] fix(agc): clamp sceAgcInit versions above 13 inline --- core/libs/prx/libSceAgc/DcbState/src/Init.cpp | 17 +++-------------- docs/dev/debt/libSceAgc.md | 2 +- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgc/DcbState/src/Init.cpp b/core/libs/prx/libSceAgc/DcbState/src/Init.cpp index 3d3d07f3c95..e73f0021895 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Init.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Init.cpp @@ -7,23 +7,12 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -namespace { - -constexpr std::uint32_t kRegisterDefaultsVersionCount = 14; -constexpr std::uint32_t kRegisterDefaultsFallbackVersion = kRegisterDefaultsVersionCount - 1; - -std::uint32_t ResolveRegisterDefaultsVersion(std::uint32_t version) { - return version < kRegisterDefaultsVersionCount ? version : kRegisterDefaultsFallbackVersion; -} - -} - extern "C" { int APS5_VABI sceAgcInit(std::uint32_t version) { - const std::uint32_t defaultsVersion = ResolveRegisterDefaultsVersion(version); - Agc::Command::GetRegisterDefaults(defaultsVersion, false, __func__); - Agc::Command::GetRegisterDefaults(defaultsVersion, true, __func__); + if (version >= 14) version = 13; + Agc::Command::GetRegisterDefaults(version, false, __func__); + Agc::Command::GetRegisterDefaults(version, true, __func__); return 0; } diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index b798ad659ea..143bd227428 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -7,7 +7,7 @@ - [sceAgcDcbSetCfRegisterDirect, sceAgcDcbSetCfRegisterRangeDirect](../../../core/libs/prx/libSceAgc/DcbState/src/Registers.cpp) (libSceAgc) - signatures follow [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Agc/SceAgc.cs), which agree; the SET_CONFIG_REG (0x68) packet is OpenAGC's. The driver does not execute SET_CONFIG_REG, so a submitted packet throws - [Command buffer cursors](../../../core/libs/prx/libSceAgc/Command/src/Packet.cpp) (libSceAgc) - a null `cursor_down` is taken as a buffer without downward allocations, whose limit is `top`. PPSA16064 builds one-packet buffers of 8 dwords that way; the console's handling is unconfirmed - [sceAgcGetRegisterDefaultsInternal](../../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - returns the version 0 internal table, as `sceAgcGetRegisterDefaults` returns the version 0 public one; [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns the table of the version passed to `sceAgcInit` instead, and null before it -- [sceAgcInit](../../../core/libs/prx/libSceAgc/DcbState/src/Init.cpp) (libSceAgc) - versions 0–13 select their register-default tables; a version of 14 or above falls back to the version 13 tables and returns 0. [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns `0x80890001` above 12 for `sceAgcInit` and falls back to version 11 in `sceAgcGetRegisterDefaults2`. Public homebrew that imports NID `kW3GLb7QfPg` with a pointer-shaped first argument (PPSA99011, PPSA99995, [ps5-vulkan](https://github.com/mpereiraesaa/ps5-vulkan), [prospero-win](https://github.com/mpereiraesaa/prospero-win)) reports success on firmware 12.02 for that call shape; which table the firmware selects and the true upper version it accepts are unverified. `sceAgcInit_0090` (NID `23LRUSvYu1M`) checks the state pointer then follows `sceAgcInit`, as OpenAGC does +- [sceAgcInit](../../../core/libs/prx/libSceAgc/DcbState/src/Init.cpp) (libSceAgc) - versions 0–13 select their tables; 14 and above use the version 13 tables and return 0. [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns `0x80890001` above 12 for `sceAgcInit`, and falls back to version 11 in `sceAgcGetRegisterDefaults2`. PPSA99011, PPSA99995, [ps5-vulkan](https://github.com/mpereiraesaa/ps5-vulkan) and [prospero-win](https://github.com/mpereiraesaa/prospero-win) call NID `kW3GLb7QfPg` with a pointer-shaped argument and report 0 on firmware 12.02; which table the firmware selects, and the highest version it accepts, are unverified. `sceAgcInit_0090` (NID `23LRUSvYu1M`) checks the state pointer and then calls `sceAgcInit`, as OpenAGC does - [sceAgcSetSubmitMode](../../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted - [sceAgcGetGsOversubscription](../../../core/libs/prx/libSceAgc/Shader/src/GsOversubscription.cpp) (libSceAgc) - signature and occupancy formula (base and expanded vertex/export capacities, budget shift, `GE_PC_ALLOC` / `SPI_SHADER_PGM_RSRC4_GS` encoding) follow KytyPS5 only, not confirmed on hardware or a title; a negative or NaN factor that gives a negative target, a missing GS context register or a zero `GE_MAX_OUTPUT_PER_SUBGROUP` throws - [sceAgcGetGsPrimPayload](../../../core/libs/prx/libSceAgc/Shader/src/GsPrimPayload.cpp) (libSceAgc) - the search does not stop at an `SPI_SHADER_IDX_FORMAT` (0x1C2) entry whose format is not `SPI_SHADER_2COMP`, as the analysis of the library declared in the pull request describes (its origin and firmware version are not identified), while [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1285-L1310) stops there; not verified on PS5 hardware. A null payload, a null shader or a register count with a null register array throws instead of faulting on the memory access From eb1b9eca96162629566803b50d3e87188a8aed66 Mon Sep 17 00:00:00 2001 From: Maz <mazenshaar@hotmail.com> Date: Wed, 14 Oct 2026 11:38:52 +0400 Subject: [PATCH 31/81] fix(agc): return 0 for out-of-range sceAgcInit without table lookup Pointer-shaped calls match the #4808 console probe (success, no writes); selecting table 13 for those values was unsupported. --- core/libs/prx/libSceAgc/DcbState/src/Init.cpp | 2 +- core/libs/prx/libSceAgc/tests/Commands.cpp | 6 +++--- docs/dev/debt/libSceAgc.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgc/DcbState/src/Init.cpp b/core/libs/prx/libSceAgc/DcbState/src/Init.cpp index e73f0021895..f3acef34c52 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Init.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Init.cpp @@ -10,7 +10,7 @@ extern "C" { int APS5_VABI sceAgcInit(std::uint32_t version) { - if (version >= 14) version = 13; + if (version >= 14) return 0; Agc::Command::GetRegisterDefaults(version, false, __func__); Agc::Command::GetRegisterDefaults(version, true, __func__); return 0; diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 82736484e6b..7c90e826440 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -625,9 +625,9 @@ void testDefaults() { check(sceAgcInit_0090(&state, 14) == 0 && state == 0x12345678, "out-of-range AGC initialization version changed caller state"); check(sceAgcInit(8) == 0, "AGC version initialization failed"); check(sceAgcInit(13) == 0, "AGC version 13 initialization failed"); - check(sceAgcInit(14) == 0, "out-of-range AGC version did not fall back"); - check(sceAgcInit(0xffffffffu) == 0, "maximum AGC version did not fall back"); - check(sceAgcInit(0xffffc278u) == 0, "pointer-valued AGC argument did not fall back"); + check(sceAgcInit(14) == 0, "out-of-range AGC version did not return success"); + check(sceAgcInit(0xffffffffu) == 0, "maximum AGC version did not return success"); + check(sceAgcInit(0xffffc278u) == 0, "pointer-valued AGC argument did not return success"); for (std::uint32_t version = 0; version < 14; ++version) { for (const bool internal : {false, true}) { auto* first = Agc::Command::GetRegisterDefaults(version, internal, __func__); diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 143bd227428..81b3f5e7183 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -7,7 +7,7 @@ - [sceAgcDcbSetCfRegisterDirect, sceAgcDcbSetCfRegisterRangeDirect](../../../core/libs/prx/libSceAgc/DcbState/src/Registers.cpp) (libSceAgc) - signatures follow [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Agc/SceAgc.cs), which agree; the SET_CONFIG_REG (0x68) packet is OpenAGC's. The driver does not execute SET_CONFIG_REG, so a submitted packet throws - [Command buffer cursors](../../../core/libs/prx/libSceAgc/Command/src/Packet.cpp) (libSceAgc) - a null `cursor_down` is taken as a buffer without downward allocations, whose limit is `top`. PPSA16064 builds one-packet buffers of 8 dwords that way; the console's handling is unconfirmed - [sceAgcGetRegisterDefaultsInternal](../../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - returns the version 0 internal table, as `sceAgcGetRegisterDefaults` returns the version 0 public one; [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns the table of the version passed to `sceAgcInit` instead, and null before it -- [sceAgcInit](../../../core/libs/prx/libSceAgc/DcbState/src/Init.cpp) (libSceAgc) - versions 0–13 select their tables; 14 and above use the version 13 tables and return 0. [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns `0x80890001` above 12 for `sceAgcInit`, and falls back to version 11 in `sceAgcGetRegisterDefaults2`. PPSA99011, PPSA99995, [ps5-vulkan](https://github.com/mpereiraesaa/ps5-vulkan) and [prospero-win](https://github.com/mpereiraesaa/prospero-win) call NID `kW3GLb7QfPg` with a pointer-shaped argument and report 0 on firmware 12.02; which table the firmware selects, and the highest version it accepts, are unverified. `sceAgcInit_0090` (NID `23LRUSvYu1M`) checks the state pointer and then calls `sceAgcInit`, as OpenAGC does +- [sceAgcInit](../../../core/libs/prx/libSceAgc/DcbState/src/Init.cpp) (libSceAgc) - versions 0–13 select their register-default tables and return 0; pointer-shaped or otherwise out-of-range values (>=14) return 0 without a register-default lookup. Console probe on firmware 13.60 ([#4808](https://github.com/boykopovar/AnyPS5/issues/4808#issuecomment-6149448503)): `sceAgcInit(&buf, 8)` (NID `kW3GLb7QfPg`) returns 0 and writes nothing; scalar versions 2–13 return `0x8A6C0004` there, while AnyPS5 still returns 0 for those scalars. The probe does not show that table 13 is selected for pointer-shaped calls. [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns `0x80890001` above 12 for `sceAgcInit`, and falls back to version 11 in `sceAgcGetRegisterDefaults2`. PPSA99011, PPSA99995, [ps5-vulkan](https://github.com/mpereiraesaa/ps5-vulkan) and [prospero-win](https://github.com/mpereiraesaa/prospero-win) call that NID with a pointer-shaped first argument. `sceAgcInit_0090` (NID `23LRUSvYu1M`) checks the state pointer and then calls `sceAgcInit`, as OpenAGC does - [sceAgcSetSubmitMode](../../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted - [sceAgcGetGsOversubscription](../../../core/libs/prx/libSceAgc/Shader/src/GsOversubscription.cpp) (libSceAgc) - signature and occupancy formula (base and expanded vertex/export capacities, budget shift, `GE_PC_ALLOC` / `SPI_SHADER_PGM_RSRC4_GS` encoding) follow KytyPS5 only, not confirmed on hardware or a title; a negative or NaN factor that gives a negative target, a missing GS context register or a zero `GE_MAX_OUTPUT_PER_SUBGROUP` throws - [sceAgcGetGsPrimPayload](../../../core/libs/prx/libSceAgc/Shader/src/GsPrimPayload.cpp) (libSceAgc) - the search does not stop at an `SPI_SHADER_IDX_FORMAT` (0x1C2) entry whose format is not `SPI_SHADER_2COMP`, as the analysis of the library declared in the pull request describes (its origin and firmware version are not identified), while [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1285-L1310) stops there; not verified on PS5 hardware. A null payload, a null shader or a register count with a null register array throws instead of faulting on the memory access From ca73d0fb321b1239ff0aaa76f8d8276689dbfbe5 Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Wed, 14 Oct 2026 03:48:28 -0400 Subject: [PATCH 32/81] test(agc): keep watched settle coverage when direct imports are unavailable Co-Authored-By: Codex <noreply@openai.com> --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 070b5f9b6cc..054dedff5fd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -4034,7 +4034,8 @@ void unchangedCpuStampTests(const Device& device, Recorder& recorder) { std::cout << "unchanged CPU stamps: ok\n"; } -bool keptUnitSettleTests(const Device& device, Recorder& recorder) { +void keptUnitSettleTests(const Device& device, Recorder& recorder) { + Require(AgcDriver::GuestMemory::WriteWatched(), "(k) kept-unit settle tests require host write watching"); const auto& base = device.GetContext(); constexpr std::uint32_t width = 1024; constexpr std::uint32_t height = 512; @@ -4042,13 +4043,8 @@ bool keptUnitSettleTests(const Device& device, Recorder& recorder) { constexpr std::size_t unitBytes = 65536; constexpr std::array<std::uint8_t, 4> red{255, 0, 0, 255}; constexpr std::array<std::uint8_t, 4> blue{0, 0, 255, 255}; - bool directTested = false; for (const bool direct : {false, true}) { const std::string what = direct ? "GPU-mapped direct memory" : "watched memory"; - if (!AgcDriver::GuestMemory::WriteWatched()) { - std::cout << "no write watching: units stored next to a unit kept for the CPU in " << what << " not tested\n"; - continue; - } if (direct && base.hostImportAlignment == 0) { std::cout << "host imports unavailable: units stored next to a unit kept for the CPU in " << what << " not tested\n"; continue; @@ -4164,9 +4160,7 @@ bool keptUnitSettleTests(const Device& device, Recorder& recorder) { Require(std::to_integer<std::uint8_t>(head[16]) == 0x77, "(k) " + what + ": the second write-back stored over unit 0, which the CPU wrote after that unit's generation"); } std::cout << "units stored next to a unit kept for the CPU in " << what << ": ok\n"; - if (direct) directTested = true; } - return directTested; } void settledBaselineTests(const Device& device, Recorder& recorder) { @@ -6515,7 +6509,8 @@ int main(int argc, char** argv) { return 0; } if (argc == 2 && std::string_view(argv[1]) == "--kept-unit-settle-only") { - return keptUnitSettleTests(device, recorder) ? 0 : 77; + keptUnitSettleTests(device, recorder); + return 0; } if (argc == 2 && std::string_view(argv[1]) == "--integer-clear-only") { integerClearTests(device); From e052811006c4c189f74f0d8f79d103631c82e718 Mon Sep 17 00:00:00 2001 From: GalaxyGamingFR <153150716+GalaxyGamingFR@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:49:22 -0700 Subject: [PATCH 33/81] fix(conventions): ignore code blocks when checking doc links The doc-link check matched C++ lambdas such as `[](int a)` inside fenced code blocks and inline code, reporting bogus missing targets. Links now require non-empty link text, and links() strips fenced blocks and inline code spans first. docs() also skips added lines that sit inside a fence of the full file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- CMakeLists.txt | 2 ++ tools/check_conventions.py | 16 +++++++++- .../tests/test_check_conventions_doc_links.py | 29 +++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 tools/tests/test_check_conventions_doc_links.py diff --git a/CMakeLists.txt b/CMakeLists.txt index b9b7abe9287..f471619def6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -113,6 +113,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_wrapped_declarations.py) add_test(NAME conventions_ci_gate COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_ci_gate.py) + add_test(NAME conventions_doc_links COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_doc_links.py) add_test(NAME test_registration COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_test_registration.py) add_test(NAME check_skips COMMAND ${Python3_EXECUTABLE} diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 61b187c9849..a75c0ae3027 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -46,7 +46,9 @@ SYSTEM_DEPENDENCY = re.compile(r"\b(find_package\s*\((?!\s*(Python3|Threads|Git)\b)|pkg_check_modules\b|pkg_search_module\b|find_(library|path|program|file)\b)", re.I) TITLE = re.compile(r"\b(PPSA|CUSA)\d{5}\b") EXTENSION = re.compile(r"__attribute__|__declspec|#\s*pragma\s+(?!once\b)") -LINK = re.compile(r"\]\(([^)\s]+)") +LINK = re.compile(r"\[[^\]]+\]\(([^)\s]+)") +FENCE = re.compile(r"^ {0,3}(```|~~~).*?^ {0,3}\1[ \t]*$|^ {0,3}(?:```|~~~).*\Z", re.S | re.M) +INLINE = re.compile(r"`[^`\n]*`") SECTION = re.compile(r"^###(?!#)\s*(.+?)\s*$", re.M) KEY = re.compile(r"^( *)([A-Za-z0-9_-]+|'[^']*'|\"[^\"]*\"):(?:\s+(.*?))?\s*$") @@ -178,10 +180,13 @@ def repository(self): def docs(self): for path, lines in self.files(lambda p: p.suffix == ".md"): + fenced = fenced_lines(self.show(path)) for number, text in lines: name = debt_name(text) if is_debt(path) and text.startswith("- ") else None if name and path != f"{DEBT_DIR}{name}.md": self.report("debt-file", path, number, f"{DEBT_DIR}{name}.md") + if number in fenced: + continue for target, resolved in links(path, text): if resolved not in self.tree and resolved not in self.dirs: self.report("doc-link", path, number, target) @@ -231,8 +236,17 @@ def utf8(data): return True +def fenced_lines(text): + inside = set() + for match in FENCE.finditer(text): + first = text.count("\n", 0, match.start()) + 1 + inside.update(range(first, first + match.group(0).count("\n") + 1)) + return inside + + def links(path, text): result = [] + text = INLINE.sub("", FENCE.sub("", text)) for target in LINK.findall(text): if re.match(r"[A-Za-z][A-Za-z0-9+.-]*:|//|#", target): continue diff --git a/tools/tests/test_check_conventions_doc_links.py b/tools/tests/test_check_conventions_doc_links.py new file mode 100644 index 00000000000..d230f88a30e --- /dev/null +++ b/tools/tests/test_check_conventions_doc_links.py @@ -0,0 +1,29 @@ +from pathlib import Path +import sys +import unittest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import check_conventions + +FENCE = "`" * 3 + + +class DocLinkTests(unittest.TestCase): + def test_code_blocks_and_inline_code_have_no_links(self): + text = f"{FENCE}cpp\nauto f = [](int a) {{ return a; }};\n{FENCE}\n~~~\nauto g = [](int b) {{}};\n~~~\nuse `[](auto)` here\n" + self.assertEqual(check_conventions.links("docs/x.md", text), []) + + def test_real_link_outside_block_is_kept(self): + text = f"{FENCE}\n[](int a)\n{FENCE}\nsee [guide](../README.md)\n" + self.assertEqual(check_conventions.links("docs/x.md", text), [("../README.md", "README.md")]) + + def test_empty_link_text_is_ignored(self): + self.assertEqual(check_conventions.links("docs/x.md", "![](img.png) and [](empty.md)"), []) + + def test_fenced_lines(self): + text = f"a\n{FENCE}\n[](int a)\n{FENCE}\nb\n" + self.assertEqual(check_conventions.fenced_lines(text), {2, 3, 4}) + + +if __name__ == "__main__": + unittest.main() From fdf92154bebe4b4286a774842354dc96f80457da Mon Sep 17 00:00:00 2001 From: ducloc080202 <ducloc080202@gmail.com> Date: Tue, 13 Oct 2026 21:49:33 +0700 Subject: [PATCH 34/81] refactor(libSceShare.native): drop duplicated exports --- core/libs/prx/libSceShare.native/Export.cpp | 80 +-------------------- 1 file changed, 1 insertion(+), 79 deletions(-) diff --git a/core/libs/prx/libSceShare.native/Export.cpp b/core/libs/prx/libSceShare.native/Export.cpp index 2d8f2cf3613..fb34206c211 100644 --- a/core/libs/prx/libSceShare.native/Export.cpp +++ b/core/libs/prx/libSceShare.native/Export.cpp @@ -3,86 +3,8 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -// Capture and sharing are not emulated; policy and parameter calls are accepted. - extern "C" { -int APS5_VABI sceShareCaptureScreenshot(const void* param, int32_t* req_id) { - (void)param; - (void)req_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceShareCaptureVideoClip(const void* param, int32_t* req_id) { - (void)param; - (void)req_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceShareFeaturePermit(uint32_t feature_flags) { - (void)feature_flags; - return 0; -} - -int APS5_VABI sceShareFeatureProhibit(uint32_t feature_flags) { - (void)feature_flags; - return 0; -} - -int APS5_VABI sceShareGetCurrentStatus(uint32_t feature_flag, ShareCurrentStatus* status) { - (void)feature_flag; - (void)status; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceShareInitialize(size_t heap_size, int thread_priority, uint64_t affinity_mask) { - (void)heap_size; - (void)thread_priority; - (void)affinity_mask; - return 0; -} - -int APS5_VABI sceShareOpenMenuForContent(const void* content_id) { - (void)content_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceShareRegisterContentEventCallback(void* callback, void* user_data) { - (void)callback; - (void)user_data; - return 0; -} - -int APS5_VABI sceShareSetCaptureSource(uint32_t feature_flags, const void* tap_point) { - (void)feature_flags; - (void)tap_point; - return 0; -} - -int APS5_VABI sceShareSetContentParam(const char* content_param) { - (void)content_param; - return 0; -} - -int APS5_VABI sceShareSetScreenshotOverlayImage(const char* file_path, int32_t margin_x, int32_t margin_y, int32_t origin) { - (void)file_path; - (void)margin_x; - (void)margin_y; - (void)origin; - return 0; -} - -int APS5_VABI sceShareTerminate(void) { - return 0; -} - -int APS5_VABI sceShareUnregisterContentEventCallback(void* callback) { - (void)callback; - return 0; -} +APS5_DUMMY_FUN } From 2e461aa2e4750911b0bee57d204e38e024eacab3 Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Mon, 12 Oct 2026 14:48:55 -0400 Subject: [PATCH 35/81] build(recompiler): generate the shader cache version as a source file ShaderCacheVersion.cmake wrote the source version and the APS5_* switch list into a generated ShaderCacheVersion.hpp that ShaderDiskCache.cpp included. The version hashes every recompiler source, so any content change under core/shader/recompiler rewrote that header and recompiled ShaderDiskCache.cpp next to the file actually edited. The script now writes ShaderCacheVersion.cpp, which defines Generated::SourceVersion and Generated::RecompilerSwitches (a span over the same sorted switch list) with constinit. They are declared in the new ShaderCacheSourceVersion.hpp in the source tree, which ShaderDiskCache.cpp includes; the generated directory is no longer an include directory. The header has a different name from the old generated one so that a build directory that still holds the stale ShaderCacheVersion.hpp next to the generated .cpp cannot pick it up. What is hashed is unchanged: the same manifest (recompiler sources, HostThreadLocal.hpp, submodule commits, compiler, build type, architecture and flags), the same checks and the same rewrite-only- when-different step, so the version still changes exactly when it did. Running the upstream/main script and this one with the same arguments over this tree gives the same version (0x7907101dc4d8c62e) and the same 26 switches, and a second ninja run has no work to do. The value differs from the one main 682ef0c17 generates (0xdce3a0ce03bf030a) only because this commit edits hashed files, as any recompiler change does. Tested with ninja -d explain -j 4 shader_recompiler_agc_driver, CCACHE_DISABLE=1, after appending a line to IntermediateRepresentation/src/IrType.cpp, three runs on main and then three runs on this commit back to back, with the machine at 100% CPU from other builds. On main ShaderCacheVersion.hpp and ShaderDiskCache.cpp.obj are dirty and the rebuild takes 22.0/23.9/22.2 s. With this change only ShaderCacheVersion.cpp.obj and IrType.cpp.obj are compiled, ShaderDiskCache.cpp is absent from the explain output, and the rebuild takes 12.2/14.7/15.0 s. The shader and recompiler ctest set (26 tests) passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Codex <noreply@openai.com> --- core/shader/recompiler/CMakeLists.txt | 9 ++++----- .../recompiler/ShaderCacheSourceVersion.hpp | 15 +++++++++++++++ core/shader/recompiler/ShaderCacheVersion.cmake | 17 ++++++++--------- core/shader/recompiler/ShaderDiskCache.cpp | 2 +- 4 files changed, 28 insertions(+), 15 deletions(-) create mode 100644 core/shader/recompiler/ShaderCacheSourceVersion.hpp diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index d4ff8cefaa9..6b11c97a3d3 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -165,7 +165,7 @@ function(add_shader_recompiler target runtimeTarget) target_compile_features(${target} PUBLIC cxx_std_20) set(shaderCacheGeneratedDir "${CMAKE_CURRENT_BINARY_DIR}/${target}_generated") - set(shaderCacheVersionHeader "${shaderCacheGeneratedDir}/ShaderCacheVersion.hpp") + set(shaderCacheVersionSource "${shaderCacheGeneratedDir}/ShaderCacheVersion.cpp") file(GLOB_RECURSE shaderCacheRecompilerSources CONFIGURE_DEPENDS "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/*.cpp" "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/*.hpp" @@ -274,21 +274,20 @@ function(add_shader_recompiler target runtimeTarget) set(shaderCacheListFile "${shaderCacheGeneratedDir}/sources.txt") file(CONFIGURE OUTPUT "${shaderCacheListFile}" CONTENT "${shaderCacheManifest}" @ONLY) add_custom_command( - OUTPUT "${shaderCacheVersionHeader}" + OUTPUT "${shaderCacheVersionSource}" COMMAND ${CMAKE_COMMAND} "-DROOT=${PROJECT_SOURCE_DIR}" "-DLIST_FILE=${shaderCacheListFile}" "-DRECOMPILER_DIR=${CMAKE_CURRENT_FUNCTION_LIST_DIR}" "-DGIT=${GIT_EXECUTABLE}" "-DEXTRA=${shaderCacheExtra}" - "-DOUTPUT=${shaderCacheVersionHeader}" + "-DOUTPUT=${shaderCacheVersionSource}" -P "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/ShaderCacheVersion.cmake" DEPENDS ${shaderCacheRecompilerSources} ${shaderCacheGuardSources} ${shaderCacheSubmoduleStamps} "${shaderCacheListFile}" "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/ShaderCacheVersion.cmake" COMMENT "Hashing the shader recompiler sources for the shader disk cache" VERBATIM ) - target_sources(${target} PRIVATE "${shaderCacheVersionHeader}") - target_include_directories(${target} PRIVATE "${shaderCacheGeneratedDir}") + target_sources(${target} PRIVATE "${shaderCacheVersionSource}") set_target_properties(${target} PROPERTIES POSITION_INDEPENDENT_CODE ON diff --git a/core/shader/recompiler/ShaderCacheSourceVersion.hpp b/core/shader/recompiler/ShaderCacheSourceVersion.hpp new file mode 100644 index 00000000000..d73de7db7fa --- /dev/null +++ b/core/shader/recompiler/ShaderCacheSourceVersion.hpp @@ -0,0 +1,15 @@ +#ifndef CORE_SHADER_RECOMPILER_SHADERCACHESOURCEVERSION_HPP +#define CORE_SHADER_RECOMPILER_SHADERCACHESOURCEVERSION_HPP + +#include <cstdint> +#include <span> +#include <string_view> + +namespace ShaderRecompiler::ShaderDiskCache::Generated { + +extern const std::uint64_t SourceVersion; +extern const std::span<const std::string_view> RecompilerSwitches; + +} + +#endif diff --git a/core/shader/recompiler/ShaderCacheVersion.cmake b/core/shader/recompiler/ShaderCacheVersion.cmake index e952236aef8..fe033d0a1f1 100644 --- a/core/shader/recompiler/ShaderCacheVersion.cmake +++ b/core/shader/recompiler/ShaderCacheVersion.cmake @@ -140,23 +140,22 @@ list(REMOVE_DUPLICATES switches) list(SORT switches) list(JOIN switches ",\n " switchList) -set(content "#ifndef CORE_SHADER_RECOMPILER_SHADERCACHEVERSION_HPP -#define CORE_SHADER_RECOMPILER_SHADERCACHEVERSION_HPP - -#include <cstdint> -#include <string_view> - +set(content "#include \"ShaderCacheSourceVersion.hpp\"\n") +string(APPEND content " namespace ShaderRecompiler::ShaderDiskCache::Generated { -inline constexpr std::uint64_t SourceVersion = 0x${version}ull; +namespace { -inline constexpr std::string_view RecompilerSwitches[] = { +constexpr std::string_view recompilerSwitches[] = { ${switchList} }; } -#endif +constinit const std::uint64_t SourceVersion = 0x${version}ull; +constinit const std::span<const std::string_view> RecompilerSwitches = recompilerSwitches; + +} ") if(EXISTS "${OUTPUT}") diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index c650f519c28..d18c6bb6bac 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -20,7 +20,7 @@ #include <sys/stat.h> #include <unistd.h> #endif -#include "ShaderCacheVersion.hpp" +#include "ShaderCacheSourceVersion.hpp" #include <algorithm> #include <atomic> #include <chrono> From 8ed0bc395dffcdff884d1d028985d12d23b77eee Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Tue, 13 Oct 2026 14:37:56 -0400 Subject: [PATCH 36/81] perf(shader): avoid redundancy traversal map copies --- .../libSceAgcDriver/tests/SpirvOptimizer.cpp | 73 ++++++++++++++ core/shader/recompiler/PatchSpirvTools.cmake | 94 ++++++++++++++++--- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 156 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp index b9c39114062..52350df247d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp @@ -378,6 +378,77 @@ OpFunctionEnd } } +void testLongControlFlow(bool optimizationEnabled, bool large) { + const char* mode = std::getenv("APS5_SPIRV_OPT"); + const bool full = mode != nullptr && std::string(mode) == "full"; + const std::size_t blocks = large && optimizationEnabled && !full ? 512 : 8; + std::string source = R"(OpCapability Shader +OpCapability StorageBuffer8BitAccess +OpExtension "SPV_KHR_8bit_storage" +OpMemoryModel Logical GLSL450 +OpEntryPoint GLCompute %main "main" +OpExecutionMode %main LocalSize 1 1 1 +OpDecorate %array ArrayStride 4 +OpMemberDecorate %buffer 0 Offset 0 +OpDecorate %buffer Block +OpDecorate %data DescriptorSet 0 +OpDecorate %data Binding 0 +OpDecorate %byteArray ArrayStride 1 +OpMemberDecorate %byteBuffer 0 Offset 0 +OpDecorate %byteBuffer Block +OpDecorate %bytes DescriptorSet 0 +OpDecorate %bytes Binding 1 +%void = OpTypeVoid +%uint = OpTypeInt 32 0 +%boolean = OpTypeBool +%byte = OpTypeInt 8 0 +%byteArray = OpTypeRuntimeArray %byte +%byteBuffer = OpTypeStruct %byteArray +%byteBufferPointer = OpTypePointer StorageBuffer %byteBuffer +%bytePointer = OpTypePointer StorageBuffer %byte +%array = OpTypeRuntimeArray %uint +%buffer = OpTypeStruct %array +%bufferPointer = OpTypePointer StorageBuffer %buffer +%valuePointer = OpTypePointer StorageBuffer %uint +%function = OpTypeFunction %void +%data = OpVariable %bufferPointer StorageBuffer +%bytes = OpVariable %byteBufferPointer StorageBuffer +%zero = OpConstant %uint 0 +%one = OpConstant %uint 1 +%two = OpConstant %uint 2 +)"; + for (std::size_t i = 0; i < blocks * 4; ++i) source += "%index" + std::to_string(i) + " = OpConstant %uint " + std::to_string(i + 1) + "\n"; + source += "%main = OpFunction %void None %function\n%entry = OpLabel\n%input = OpAccessChain %valuePointer %data %zero %zero\n%seed = OpLoad %uint %input Volatile\n%condition = OpINotEqual %boolean %seed %zero\nOpBranch %block0\n"; + for (std::size_t i = 0; i < blocks; ++i) { + const auto number = std::to_string(i); + const auto previous = i == 0 ? "%seed" : "%next" + std::to_string(i - 1); + source += "%block" + number + " = OpLabel\n%sum" + number + " = OpIAdd %uint " + previous + " %one\n"; + source += "%duplicate" + number + " = OpIAdd %uint " + previous + " %one\n%shared" + number + " = OpIAdd %uint %seed %one\n"; + for (std::size_t j = 0; j < 3; ++j) { + const auto index = std::to_string(i * 4 + j); + source += "%slot" + index + " = OpAccessChain %valuePointer %data %zero %index" + index + "\n"; + source += "OpStore %slot" + index + " %" + (j == 0 ? "sum" : j == 1 ? "duplicate" : "shared") + number + " Volatile\n"; + } + const auto branchIndex = std::to_string(i * 4 + 3); + source += "%branchSlot" + number + " = OpAccessChain %valuePointer %data %zero %index" + branchIndex + "\n"; + source += "OpSelectionMerge %join" + number + " None\nOpBranchConditional %condition %left" + number + " %right" + number + "\n"; + source += "%left" + number + " = OpLabel\n%leftValue" + number + " = OpIAdd %uint %sum" + number + " %one\nOpStore %branchSlot" + number + " %leftValue" + number + " Volatile\nOpBranch %join" + number + "\n"; + source += "%right" + number + " = OpLabel\n%rightCopy" + number + " = OpIAdd %uint %sum" + number + " %one\nOpStore %branchSlot" + number + " %rightCopy" + number + " Volatile\n"; + source += "%rightValue" + number + " = OpIAdd %uint %sum" + number + " %two\nOpStore %branchSlot" + number + " %rightValue" + number + " Volatile\nOpBranch %join" + number + "\n"; + source += "%join" + number + " = OpLabel\n%next" + number + " = OpPhi %uint %leftValue" + number + " %left" + number + " %rightValue" + number + " %right" + number + "\n"; + if (i + 1 == blocks) source += "%byteSlot = OpAccessChain %bytePointer %bytes %zero %zero\n%truncated = OpUConvert %byte %next" + number + "\nOpStore %byteSlot %truncated Volatile\nOpReturn\n"; + else source += "OpBranch %block" + std::to_string(i + 1) + "\n"; + } + source += "OpFunctionEnd\n"; + const auto input = assemble(source); + const auto output = ShaderRecompiler::ValidateAndOptimizeSpirv(input, Vulkan11, Spirv13, false); + const auto stores = opcodeCount(output, spv::OpStore); + check(stores >= blocks * 4 + 1 && stores <= blocks * 6 + 1, "long control-flow optimization discarded observable stores"); + const auto adds = opcodeCount(output, spv::OpIAdd); + if (!optimizationEnabled) check(output == input, "disabled optimization changed the long control-flow module"); + else check(adds >= blocks && adds <= blocks * 4, "control-flow optimization lost arithmetic or retained redundant values"); +} + } int main() { @@ -388,6 +459,8 @@ int main() { testInvalidInputIsRejected(); testSpecializedSelectionExit(); testSpecializedDeadContinue(); + testLongControlFlow(optimizationEnabled, false); + testLongControlFlow(optimizationEnabled, true); if (failures != 0) { std::fprintf(stderr, "%d optimizer check(s) failed\n", failures); return 1; diff --git a/core/shader/recompiler/PatchSpirvTools.cmake b/core/shader/recompiler/PatchSpirvTools.cmake index 2a6e17df0e8..69cb4bcfc79 100644 --- a/core/shader/recompiler/PatchSpirvTools.cmake +++ b/core/shader/recompiler/PatchSpirvTools.cmake @@ -3,31 +3,101 @@ set(spirvToolsRedundancySource "${spirvToolsSourceDir}/redundancy_elimination.cp set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${spirvToolsRedundancySource}") file(READ "${spirvToolsRedundancySource}" spirvToolsRedundancy) +set(spirvToolsLocalRedundancySource "${spirvToolsSourceDir}/local_redundancy_elimination.cpp") +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${spirvToolsLocalRedundancySource}") +file(READ "${spirvToolsLocalRedundancySource}" spirvToolsLocalRedundancy) +set(spirvToolsExpectedElimination [=[ + auto func = [this, &vnTable, &modified, value_to_ids](Instruction* inst) { + if (inst->result_id() == 0) { + return; + } + + uint32_t value = vnTable.GetValueNumber(inst); + + if (value == 0) { + return; + } + + auto candidate = value_to_ids->insert({value, inst->result_id()}); + if (!candidate.second) { + context()->KillNamesAndDecorates(inst); + context()->ReplaceAllUsesWith(inst->result_id(), candidate.first->second); + context()->KillInst(inst); + modified = true; + } + }; + block->ForEachInst(func); +]=]) +string(FIND "${spirvToolsLocalRedundancy}" "${spirvToolsExpectedElimination}" spirvToolsEliminationPosition) +if(spirvToolsEliminationPosition EQUAL -1) + message(FATAL_ERROR "SPIRV-Tools value elimination has changed; review the local traversal fix") +endif() + set(spirvToolsOldTraversal [=[ + struct State { + DominatorTreeNode* node; + std::map<uint32_t, uint32_t> value_to_id_map; + }; + std::vector<State> todo; + todo.push_back({bb, std::map<uint32_t, uint32_t>()}); + bool modified = false; + for (size_t next_node = 0; next_node < todo.size(); next_node++) { + modified |= EliminateRedundanciesInBB(todo[next_node].node->bb_, vnTable, + &todo[next_node].value_to_id_map); for (DominatorTreeNode* child : todo[next_node].node->children_) { todo.push_back({child, todo[next_node].value_to_id_map}); } + } ]=]) set(spirvToolsNewTraversal [=[ - const auto& children = todo[next_node].node->children_; - for (size_t child_index = 0; child_index < children.size(); ++child_index) { - if (child_index + 1 == children.size()) { - todo.push_back({children[child_index], - std::move(todo[next_node].value_to_id_map)}); - } else { - todo.push_back({children[child_index], todo[next_node].value_to_id_map}); - } + struct State { + DominatorTreeNode* node; + size_t next_child; + size_t added_begin; + bool entered; + }; + std::map<uint32_t, uint32_t> value_to_id_map; + std::vector<uint32_t> added_values; + std::vector<State> todo; + todo.push_back({bb, 0, 0, false}); + bool modified = false; + while (!todo.empty()) { + auto& state = todo.back(); + if (!state.entered) { + state.entered = true; + state.node->bb_->ForEachInst([&](Instruction* inst) { + if (inst->result_id() == 0) return; + const uint32_t value = vnTable.GetValueNumber(inst); + if (value == 0) return; + auto candidate = value_to_id_map.insert({value, inst->result_id()}); + if (candidate.second) { + added_values.push_back(value); + } else { + context()->KillNamesAndDecorates(inst); + context()->ReplaceAllUsesWith(inst->result_id(), candidate.first->second); + context()->KillInst(inst); + modified = true; + } + }); + } + const auto& children = state.node->children_; + if (state.next_child != children.size()) { + auto* child = children[state.next_child++]; + todo.push_back({child, 0, added_values.size(), false}); + continue; + } + while (added_values.size() != state.added_begin) { + value_to_id_map.erase(added_values.back()); + added_values.pop_back(); } - todo[next_node].value_to_id_map.clear(); + todo.pop_back(); + } ]=]) string(FIND "${spirvToolsRedundancy}" "${spirvToolsOldTraversal}" spirvToolsTraversalPosition) if(spirvToolsTraversalPosition EQUAL -1) message(FATAL_ERROR "SPIRV-Tools redundancy traversal has changed; review the local memory fix") endif() string(REPLACE "${spirvToolsOldTraversal}" "${spirvToolsNewTraversal}" spirvToolsRedundancy "${spirvToolsRedundancy}") -string(REPLACE "#include \"source/opt/redundancy_elimination.h\"\n" - "#include \"source/opt/redundancy_elimination.h\"\n\n#include <utility>\n" - spirvToolsRedundancy "${spirvToolsRedundancy}") set(spirvToolsPatchedDir "${CMAKE_BINARY_DIR}/3rdparty/SPIRV-Tools/patched") set(spirvToolsPatchedSource "${spirvToolsPatchedDir}/redundancy_elimination.cpp") diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 9119b5f1141..b6c92fcc94e 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -4,7 +4,7 @@ Entries about one library or component are in [debt](debt/), one file per librar ### Build -- The bundled [SPIRV-Tools build](../../core/shader/recompiler/PatchSpirvTools.cmake) replaces the redundancy-elimination source with a build-directory copy that releases processed traversal maps. Remove this patch when the pinned revision includes [SPIRV-Tools #6962](https://github.com/KhronosGroup/SPIRV-Tools/pull/6962). System-dependency builds need the fix in their installed SPIRV-Tools package. +- The bundled [SPIRV-Tools build](../../core/shader/recompiler/PatchSpirvTools.cmake) replaces the redundancy-elimination source with a build-directory copy. A depth-first traversal keeps one value map and removes each subtree's added values on return, avoiding growing map copies and preventing sibling values from leaking into one another. This extends the processed-map release fix in [SPIRV-Tools #6962](https://github.com/KhronosGroup/SPIRV-Tools/pull/6962). Remove the patch when the pinned dependency has equivalent copy-free traversal; #6962 alone still copies maps for sibling branches. System-dependency builds need an equivalent fix in their installed package. Optimization passes and validation remain unchanged; wider shader and game performance measurement is needed. - Building on Windows requires a specific version of mingw - MinGW-w64 GCC 15.2.0 (`winlibs-gcc15`, `x86_64-ucrt-posix-seh`) - Even compiled prx libraries on Windows require nearby (static linking of these dependencies causes conflicts): - libgcc_s_seh-1.dll From fd192b20cc7169f057179da4820b6b5e694e6674 Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Tue, 13 Oct 2026 14:45:13 -0400 Subject: [PATCH 37/81] docs(shader): place traversal debt with the component --- docs/dev/TechnicalDebt.md | 1 - docs/dev/debt/shader.md | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b6c92fcc94e..4eddc9623dd 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -4,7 +4,6 @@ Entries about one library or component are in [debt](debt/), one file per librar ### Build -- The bundled [SPIRV-Tools build](../../core/shader/recompiler/PatchSpirvTools.cmake) replaces the redundancy-elimination source with a build-directory copy. A depth-first traversal keeps one value map and removes each subtree's added values on return, avoiding growing map copies and preventing sibling values from leaking into one another. This extends the processed-map release fix in [SPIRV-Tools #6962](https://github.com/KhronosGroup/SPIRV-Tools/pull/6962). Remove the patch when the pinned dependency has equivalent copy-free traversal; #6962 alone still copies maps for sibling branches. System-dependency builds need an equivalent fix in their installed package. Optimization passes and validation remain unchanged; wider shader and game performance measurement is needed. - Building on Windows requires a specific version of mingw - MinGW-w64 GCC 15.2.0 (`winlibs-gcc15`, `x86_64-ucrt-posix-seh`) - Even compiled prx libraries on Windows require nearby (static linking of these dependencies causes conflicts): - libgcc_s_seh-1.dll diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 1aca9b11e98..40467bd2508 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -117,3 +117,7 @@ - [BDA span reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) (shader recompiler) - `read_bda_span` is one selection: one arm calls `load_bda_span`, a `DontInline` function that holds the in-place dword loads and calls nothing, and the other arm is the per-dword byte fallback. Two other shapes of the same reads each hang a group of NVIDIA drivers, where a dispatch never finishes and the device is lost. With the loads emitted in the arm itself, a dispatch whose lanes took both arms hung on an RTX 3060 and an RTX 4060 Laptop GPU (Windows, driver 591.86) and an RTX 5060 (Linux, driver 590.48.01) ([#3291](https://github.com/boykopovar/AnyPS5/issues/3291)). With the loads and the fallback as two selections, the first under the span's condition and the second under its negation, it hung on an RTX 4070 Laptop GPU (Windows, drivers 595.97 and 596.36), an RTX 5080 (Windows, driver 610.62) and an RTX 4090 (Linux, driver 610.57.04) ([#4975](https://github.com/boykopovar/AnyPS5/issues/4975)). The current shape is measured on the RTX 4090 with driver 610.57.04 only, where it does not hang with or without SPIRV-Tools; a shape with the loads in their own `DontInline` function was reported passing on driver 591.86 in #3291. The cause inside the driver is not known. - [SDWA source selects on `v_cvt_f32_ubyte1`, `v_cvt_f32_ubyte2` and `v_cvt_f32_ubyte3`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (shader recompiler) - byte N is taken from source 0 after `SRC0_SEL` and `SRC0_SEXT` select and extend it, since SDWA supplies source 0 (RDNA 2 ISA guide 6.8 and 13.3.7) and the opcode reads `S0.u[8N+7:8N]` (12.8). Not measured on hardware. - [Flag structurization](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a graph that every structurizer attempt refuses, irreducible and non-terminating graphs included, is rebuilt with one reach flag per block and one again flag per loop (`structurizeWithFlags`): each block keeps one copy and runs behind a guard on its flag, and each strongly connected component becomes a loop whose single latch repeats it while a back edge set its flag. The output has about 8 times the input's blocks and one route variable per block, and ends in an empty Return that never runs. Only blocks that start the top-level order unconditionally run unguarded, so resource planning also sees the static paths that skip a guarded block and can plan extra descriptor candidates for them. The attempts still run first. Shared merge splitting refuses an attempt once the graph has grown past 16 times its block count at the start of splitting (at least 64 blocks); succeeding attempts grew at most 10.6 times on random graphs of up to 128 blocks, and an attempt that would succeed only beyond the budget leaves the graph to a later attempt or to this construction. Splitting that adds one block per round stays within that budget until its 4 rounds per block run out, so the attempts can still take more than 10 s (9 of 2,000 random graphs of up to 128 blocks, 63 s at most). `APS5_FORCE_STRUCTURIZER_FALLBACK=1` builds every graph this way, for tests + +### Build + +- The bundled [SPIRV-Tools build](../../../core/shader/recompiler/PatchSpirvTools.cmake) replaces the redundancy-elimination source with a build-directory copy. A depth-first traversal keeps one value map and removes each subtree's added values on return, avoiding growing map copies and preventing sibling values from leaking into one another. This extends the processed-map release fix in [SPIRV-Tools #6962](https://github.com/KhronosGroup/SPIRV-Tools/pull/6962). Remove the patch when the pinned dependency has equivalent copy-free traversal; #6962 alone still copies maps for sibling branches. System-dependency builds need an equivalent fix in their installed package. Optimization passes and validation remain unchanged; wider shader and game performance measurement is needed. From b00931842321f466b8ce1e7a48ac952d4f06f1ec Mon Sep 17 00:00:00 2001 From: JJenkx <josh_jenkins@hotmail.com> Date: Tue, 13 Oct 2026 19:36:52 -0400 Subject: [PATCH 38/81] docs(shader): place traversal debt in existing Build section --- docs/dev/debt/shader.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 40467bd2508..731cd62f0df 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -4,6 +4,8 @@ - winpthreads initializes a statically initialized rwlock on its first lock and fails a concurrent first lock with `EINVAL`, which `std::shared_mutex` and `std::shared_timed_mutex` ignore, so that lock does not exclude. The host `std::shared_mutex` objects in [Recompiler.cpp](../../../core/shader/recompiler/Recompiler.cpp) are locked once when created. A new host `std::shared_mutex` used on Windows needs the same. +- The bundled [SPIRV-Tools build](../../../core/shader/recompiler/PatchSpirvTools.cmake) replaces the redundancy-elimination source with a build-directory copy. A depth-first traversal keeps one value map and removes each subtree's added values on return, avoiding growing map copies and preventing sibling values from leaking into one another. This extends the processed-map release fix in [SPIRV-Tools #6962](https://github.com/KhronosGroup/SPIRV-Tools/pull/6962). Remove the patch when the pinned dependency has equivalent copy-free traversal; #6962 alone still copies maps for sibling branches. System-dependency builds need an equivalent fix in their installed package. Optimization passes and validation remain unchanged; wider shader and game performance measurement is needed. + ### Unknown function info - [Packed pixel ancillary word](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) (shader recompiler) - the sample index at bits 8-11 and the render target index at bits 16-28 follow #295. The sample index is the SampleId builtin, an equality inferred, not measured. The render target index is the Layer builtin when the draw exports it (a layered draw, `PA_CL_VS_OUT_CNTL` bit 18; the pipeline specialization constant `LayerExported`) and 0 otherwise, the first layer Vulkan renders when no pre-rasterization stage writes Layer. Reading the unwritten Layer builtin instead makes AMD's Windows driver drop the fragment's writes (#4800); with the constant at 0 the load is folded away. Mesa reads both fields from those bits, with the layer 14 bits wide only from GFX12 on ([ac_nir_lower_intrinsics_to_args.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/amd/common/nir/ac_nir_lower_intrinsics_to_args.c#L342-L368)). A value passed through a select, move or phi whose every read is an extract inside one of those fields is rebuilt from the two fields alone. Any other read the masked-select pass cannot drop gets the whole word: the primitive class at bits 0-1 (0 point, 1 line, 2 triangle, 3 rectangle), which the draw sets as a pipeline specialization constant ([RasterPrimitiveOf](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp)) in the shaders that read it, plus the sample field, with every other bit zero. That layout comes from the RDNA 3 and 3.5 ISA guides (prim type 1:0, VRS rates 3:2 and 5:4, eye id 7, sample 11:8, render target index 28:16). The GCN register guides give the same prim type and sample fields and put the render target index at 26:16, so the two sources agree for layers below 2048. AMD's LLPC reads the primitive class from bits 1:0 with the same encoding ([LowerGlCompatibility.cpp](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/llpc/lowering/LowerGlCompatibility.cpp#L830-L836)) and without a GFX-level check ([getPrimType](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/LowerInOut.cpp#L5307-L5315)). Nobody has checked it on gfx1013. In the whole word only bits 0-1 and 8-11 have a source. Bits 2-7, 12-15 and 29-31 are assumed zero, not measured: the VRS rates (2-5) and the eye id (7) because the driver supports neither variable-rate shading nor multiview, so a title that enables either would read other bits on the console, and bits 6, 12-15 and 29-31 because no source gives them a meaning. The sample field reads SampleId, which is always 0 here because the driver rejects MSAA and per-sample shading (`PA_SC_AA_CONFIG`, `PS_ITER_SAMPLE`). The whole word has the same 0 at bits 16-28. A change that makes draws export a layer must restore the Layer read in both places @@ -117,7 +119,3 @@ - [BDA span reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) (shader recompiler) - `read_bda_span` is one selection: one arm calls `load_bda_span`, a `DontInline` function that holds the in-place dword loads and calls nothing, and the other arm is the per-dword byte fallback. Two other shapes of the same reads each hang a group of NVIDIA drivers, where a dispatch never finishes and the device is lost. With the loads emitted in the arm itself, a dispatch whose lanes took both arms hung on an RTX 3060 and an RTX 4060 Laptop GPU (Windows, driver 591.86) and an RTX 5060 (Linux, driver 590.48.01) ([#3291](https://github.com/boykopovar/AnyPS5/issues/3291)). With the loads and the fallback as two selections, the first under the span's condition and the second under its negation, it hung on an RTX 4070 Laptop GPU (Windows, drivers 595.97 and 596.36), an RTX 5080 (Windows, driver 610.62) and an RTX 4090 (Linux, driver 610.57.04) ([#4975](https://github.com/boykopovar/AnyPS5/issues/4975)). The current shape is measured on the RTX 4090 with driver 610.57.04 only, where it does not hang with or without SPIRV-Tools; a shape with the loads in their own `DontInline` function was reported passing on driver 591.86 in #3291. The cause inside the driver is not known. - [SDWA source selects on `v_cvt_f32_ubyte1`, `v_cvt_f32_ubyte2` and `v_cvt_f32_ubyte3`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (shader recompiler) - byte N is taken from source 0 after `SRC0_SEL` and `SRC0_SEXT` select and extend it, since SDWA supplies source 0 (RDNA 2 ISA guide 6.8 and 13.3.7) and the opcode reads `S0.u[8N+7:8N]` (12.8). Not measured on hardware. - [Flag structurization](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a graph that every structurizer attempt refuses, irreducible and non-terminating graphs included, is rebuilt with one reach flag per block and one again flag per loop (`structurizeWithFlags`): each block keeps one copy and runs behind a guard on its flag, and each strongly connected component becomes a loop whose single latch repeats it while a back edge set its flag. The output has about 8 times the input's blocks and one route variable per block, and ends in an empty Return that never runs. Only blocks that start the top-level order unconditionally run unguarded, so resource planning also sees the static paths that skip a guarded block and can plan extra descriptor candidates for them. The attempts still run first. Shared merge splitting refuses an attempt once the graph has grown past 16 times its block count at the start of splitting (at least 64 blocks); succeeding attempts grew at most 10.6 times on random graphs of up to 128 blocks, and an attempt that would succeed only beyond the budget leaves the graph to a later attempt or to this construction. Splitting that adds one block per round stays within that budget until its 4 rounds per block run out, so the attempts can still take more than 10 s (9 of 2,000 random graphs of up to 128 blocks, 63 s at most). `APS5_FORCE_STRUCTURIZER_FALLBACK=1` builds every graph this way, for tests - -### Build - -- The bundled [SPIRV-Tools build](../../../core/shader/recompiler/PatchSpirvTools.cmake) replaces the redundancy-elimination source with a build-directory copy. A depth-first traversal keeps one value map and removes each subtree's added values on return, avoiding growing map copies and preventing sibling values from leaking into one another. This extends the processed-map release fix in [SPIRV-Tools #6962](https://github.com/KhronosGroup/SPIRV-Tools/pull/6962). Remove the patch when the pinned dependency has equivalent copy-free traversal; #6962 alone still copies maps for sibling branches. System-dependency builds need an equivalent fix in their installed package. Optimization passes and validation remain unchanged; wider shader and game performance measurement is needed. From 52813787cfcfc25840e2768ba4147ce70fb88362 Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Wed, 14 Oct 2026 22:33:41 +1300 Subject: [PATCH 39/81] fix(libkernel): defer raises while a thread holds the address-wait lock on Windows --- .../SyncOnAddress/src/SyncOnAddress.cpp | 39 ++++++-- core/libs/prx/libkernel/Time/TimedWait.cpp | 17 ++-- .../prx/libkernel/Time/include/TimedWait.hpp | 9 ++ core/libs/tests/GuestRaiseException.cpp | 90 +++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 5 files changed, 145 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp index dda99c2edfe..178c6459e0d 100644 --- a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp +++ b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp @@ -59,12 +59,26 @@ bool IsAlignedAddress(std::uintptr_t address) { return address != 0 && address % alignof(TValue) == 0; } +template <class TFunction> +void DeferringDelivery(TFunction function) { +#ifdef _WIN32 + { + TimedWait::DeliveryDeferral deferral; + function(); + } + TimedWait::DeliverDeferred(); +#else + function(); +#endif +} + template <class TValue> -bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional<std::uint64_t> deadlineNanos, const void* caller) { +std::optional<std::chrono::nanoseconds> WaitOnAddressLocked(TValue* address, TValue expected, std::optional<std::uint64_t> deadlineNanos, bool& woken) { const auto key = reinterpret_cast<std::uintptr_t>(address); std::unique_lock<std::mutex> lock(g_waitersLock); if (std::atomic_ref<TValue>(*address).load() != expected) { - return true; + woken = true; + return std::nullopt; } AddressWaiter waiter; @@ -79,15 +93,24 @@ bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional<std::uin } const auto waited = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - waitStart); - const bool woken = waiter.woken; + woken = waiter.woken; if (!woken) { queue.erase(position); if (queue.empty()) { g_waiters.erase(key); } } - lock.unlock(); - KernelTraceWait_nid_postfix("addr", caller, static_cast<std::uint64_t>(waited.count()), !woken); + return waited; +} + +template <class TValue> +bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional<std::uint64_t> deadlineNanos, const void* caller) { + bool woken = false; + std::optional<std::chrono::nanoseconds> waited; + DeferringDelivery([&] { waited = WaitOnAddressLocked(address, expected, deadlineNanos, woken); }); + if (waited) { + KernelTraceWait_nid_postfix("addr", caller, static_cast<std::uint64_t>(waited->count()), !woken); + } return woken; } @@ -97,7 +120,7 @@ int WaitOnAddress(TValue* address, TValue expected, const KernelUseconds* timeou return WaitOnAddressUntil(address, expected, deadline, caller) ? SYNC_ON_ADDRESS_OK : SCE_KERNEL_ERROR_ETIMEDOUT; } -void WakeAddress(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) { +void WakeAddressLocked(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) { std::lock_guard<std::mutex> lock(g_waitersLock); const auto entry = g_waiters.find(key); if (entry == g_waiters.end()) { @@ -115,6 +138,10 @@ void WakeAddress(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) { } } +void WakeAddress(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) { + DeferringDelivery([&] { WakeAddressLocked(key, count, wakesAtLeastOne); }); +} + std::uint64_t SaturatingNanos(std::int64_t seconds, std::int64_t nanos) { if (seconds < 0) { return 0; diff --git a/core/libs/prx/libkernel/Time/TimedWait.cpp b/core/libs/prx/libkernel/Time/TimedWait.cpp index 8535253896c..c4ae764f64b 100644 --- a/core/libs/prx/libkernel/Time/TimedWait.cpp +++ b/core/libs/prx/libkernel/Time/TimedWait.cpp @@ -241,6 +241,18 @@ void Condition::NotifyAll() { } } +DeliveryDeferral::DeliveryDeferral() { + WaitState().bound->fetch_add(1, std::memory_order_seq_cst); +} + +DeliveryDeferral::~DeliveryDeferral() { + WaitState().bound->fetch_sub(1, std::memory_order_seq_cst); +} + +void DeliverDeferred() { + DrainApcs(); +} + namespace { struct alignas(64) ParkBucket { @@ -256,11 +268,6 @@ ParkBucket& ParkBucketFor(const void* key) { return parkBuckets[(reinterpret_cast<std::uintptr_t>(key) * 0x9E3779B97F4A7C15ULL) >> 56]; } -struct DeliveryDeferral { - DeliveryDeferral() { WaitState().bound->fetch_add(1, std::memory_order_seq_cst); } - ~DeliveryDeferral() { WaitState().bound->fetch_sub(1, std::memory_order_seq_cst); } -}; - void UnlinkParked(ParkBucket& bucket, Waiter* waiter) { if (waiter->previous) waiter->previous->next = waiter->next; else bucket.head = waiter->next; diff --git a/core/libs/prx/libkernel/Time/include/TimedWait.hpp b/core/libs/prx/libkernel/Time/include/TimedWait.hpp index 4f7ccc32b51..446b0c73f31 100644 --- a/core/libs/prx/libkernel/Time/include/TimedWait.hpp +++ b/core/libs/prx/libkernel/Time/include/TimedWait.hpp @@ -22,6 +22,15 @@ struct Waiter; #ifdef _WIN32 constexpr unsigned PARK_SPINS = 1000; +class DeliveryDeferral { +public: + DeliveryDeferral(); + ~DeliveryDeferral(); + DeliveryDeferral(const DeliveryDeferral&) = delete; + DeliveryDeferral& operator=(const DeliveryDeferral&) = delete; +}; + +void DeliverDeferred(); void SpinPause(); Waiter* BeginPark(const void* key); bool FinishPark(const void* key, Waiter* waiter, const std::uint64_t* deadlineNanos); diff --git a/core/libs/tests/GuestRaiseException.cpp b/core/libs/tests/GuestRaiseException.cpp index ee3bde77068..c1b6d0a2987 100644 --- a/core/libs/tests/GuestRaiseException.cpp +++ b/core/libs/tests/GuestRaiseException.cpp @@ -3,6 +3,7 @@ #include <chrono> #include <cstddef> #include <cstdint> +#include <cstdio> #include <cstdlib> #include <cstring> #include <mutex> @@ -33,6 +34,7 @@ int APS5_VABI scePthreadRwlockDestroy(PthreadRwlock* rwlock); int APS5_VABI scePthreadRwlockWrlock(PthreadRwlock* rwlock); int APS5_VABI scePthreadRwlockUnlock(PthreadRwlock* rwlock); int APS5_VABI sceKernelSyncOnAddressWait(std::uint32_t* address, std::uint32_t expected, const KernelUseconds* timeout, const char* name); +int APS5_VABI sceKernelSyncOnAddressWake(void* address, std::int32_t count); } static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast<int>(0x80020016); @@ -402,6 +404,87 @@ static void NestedWaitKeepsOuterWake() { Require(sceKernelDeleteSema(sem) == 0); } +#ifdef _WIN32 +static constexpr int AddressRounds = 100; +static std::uint32_t addressWord = 0; +static std::uint32_t otherAddressWord = 0; +static std::atomic<bool> holdingEntered{false}; +static std::atomic<bool> holdingRelease{false}; +static std::atomic<bool> holdingLeft{false}; +static std::atomic<std::thread::id> holdingThread; + +static void APS5_VABI HoldingHandler(int signum, void*) { + Require(signum == SIGUSR1); + holdingThread.store(std::this_thread::get_id()); + holdingEntered.store(true); + while (!holdingRelease.load()) std::this_thread::yield(); + holdingLeft.store(true); +} + +static void* APS5_VABI AddressWaking(void* arg) { + auto& worker = *static_cast<Worker*>(arg); + worker.id = std::this_thread::get_id(); + worker.started.store(true); + while (!worker.stop.load()) Require(sceKernelSyncOnAddressWake(&addressWord, 1) == 0); + return nullptr; +} + +static void* APS5_VABI AddressWaiting(void* arg) { + auto& worker = *static_cast<Worker*>(arg); + worker.id = std::this_thread::get_id(); + worker.started.store(true); + while (!worker.stop.load()) { + KernelUseconds timeout = 20; + const int result = sceKernelSyncOnAddressWait(&addressWord, 0, &timeout, "address waiting"); + Require(result == 0 || result == SCE_KERNEL_ERROR_ETIMEDOUT); + } + return nullptr; +} + +static bool AwaitWithin(const std::atomic<bool>& flag, std::chrono::milliseconds limit) { + const auto deadline = std::chrono::steady_clock::now() + limit; + while (!flag.load()) { + if (std::chrono::steady_clock::now() >= deadline) return false; + std::this_thread::yield(); + } + return true; +} + +static void RequireInRound(bool value, const char* name, int round, const char* check) { + if (value) return; + std::fprintf(stderr, "%s round %d: %s\n", name, round, check); + std::fflush(stderr); + std::abort(); +} + +static void HandlerLeavesAddressLock(PthreadEntry entry, const char* name) { + Worker worker; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, entry, &worker, name) == 0); + while (!worker.started.load()) std::this_thread::yield(); + for (int round = 0; round < AddressRounds; ++round) { + holdingEntered.store(false); + holdingLeft.store(false); + holdingRelease.store(false); + RequireInRound(sceKernelRaiseException(thread, 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<bool> woke{false}; + std::thread waker([&] { + Require(sceKernelSyncOnAddressWake(&otherAddressWord, 1) == 0); + woke.store(true); + }); + const bool returned = AwaitWithin(woke, std::chrono::milliseconds(500)); + holdingRelease.store(true); + waker.join(); + RequireInRound(returned, name, round, "a wake on another address blocked for 500 ms while the handler ran"); + RequireInRound(AwaitWithin(holdingLeft, std::chrono::seconds(5)), name, round, "the handler did not return within 5 s of its release"); + } + worker.stop.store(true); + Require(scePthreadJoin(thread, nullptr) == 0); +} +#endif + static void ExpectDelivery(int before, std::thread::id thread) { for (int attempt = 0; attempt < 5000 && calls.load() == before; ++attempt) std::this_thread::sleep_for(std::chrono::milliseconds(1)); Require(calls.load() == before + 1); @@ -660,4 +743,11 @@ int main() { NestedWaitKeepsLaterWaiters(); NestedWaitKeepsOuterWake(); Require(sceKernelRemoveExceptionHandler(SIGUSR1) == 0); + +#ifdef _WIN32 + Require(sceKernelInstallExceptionHandler(SIGUSR1, reinterpret_cast<void*>(&HoldingHandler)) == 0); + HandlerLeavesAddressLock(AddressWaking, "address waking"); + HandlerLeavesAddressLock(AddressWaiting, "address waiting"); + Require(sceKernelRemoveExceptionHandler(SIGUSR1) == 0); +#endif } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index b3b9adbe261..08e033e9060 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -129,7 +129,7 @@ - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) does not consult the target thread's signal mask - [pthread_sigmask](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) shares the process-wide mask of `sigprocmask` and `_sigprocmask`, which keeps only the first 32 signals; FreeBSD keeps a mask per thread - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) reaches a thread that waits in an alertable `TimedWait` wait through an APC. A thread blocked anywhere else, including all waits under `APS5_COARSE_TIMED_WAITS` and host waits inside the emulator, is redirected and runs the handler only when that wait returns -- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs the handler wherever the target thread was interrupted. It waits only while the thread is inside winpthreads or restoring a context through `NtContinue`, and throws if that lasts 1 s, so a handler that calls into other host code the thread was interrupted in, such as the C runtime's heap or a lock of the emulator, can deadlock on it or reenter it +- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs the handler wherever the target thread was interrupted. It waits only while the thread is inside winpthreads or restoring a context through `NtContinue`, and throws if that lasts 1 s, so a handler that calls into other host code the thread was interrupted in, such as the C runtime's heap or a lock of the emulator, can deadlock on it or reenter it. A thread that holds the address-wait lock of `sceKernelSyncOnAddressWait`, `sceKernelSyncOnAddressWake` and `_umtx_op`, or the park queue lock of the guest mutexes, gets the raise as an APC instead, which runs once it has released the lock or while it waits; the other locks of the emulator are not deferred this way - [sigaltstack](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records each thread's alternate signal stack and reports it back, following FreeBSD's `kern_sigaltstack` (`EINVAL` for flags other than `SS_DISABLE`, `ENOMEM` below `MINSIGSTKSZ` 2048), but handlers never run on it, so `SS_ONSTACK` and `EPERM` are never reported - [sigpending](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) reports the intersection of the process-wide pending set of `sigtimedwait` and the blocked mask of `_sigprocmask`, as FreeBSD's `sys_sigpending` intersects the pending list with `td_sigmask`, and returns -1 with `EFAULT` for a null set. Because `_sigprocmask` keeps only the first 32 signals, signals 33 to 128 are never blocked and never appear in the set, and setting `SIG_IGN` (or `SIG_DFL` on a signal whose default action is to ignore) does not clear a signal that was already marked pending. PPSA99420 imports it - [scePthreadJoin, scePthreadCreate](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - on a PS5 (firmware 13.60) joining a thread a second time returns `ESRCH` (`0x80020003`); here a joined thread is released, so a second join reads freed memory. A null entry crashed the payload and still throws. Joining the calling thread returns `EDEADLK` and a zeroed attribute creates a default thread, as on the console From 4b71846fab8ad8ae1d56d27beb739bafa3960fe2 Mon Sep 17 00:00:00 2001 From: Nicolas Abosch <88494836+nicolasabosch@users.noreply.github.com> Date: Mon, 12 Oct 2026 19:11:12 -0300 Subject: [PATCH 40/81] feat(agc): sample a multisampled color target through its 2D MSAA texture A shader that loads from a 2D multisampled image whose T# names a color target rendered on the host is bound to a view of that target's image. Other multisampled textures keep throwing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 + .../Graphics/include/GuestTextureResource.hpp | 4 +- .../Graphics/include/MultisampleTarget.hpp | 4 + .../Graphics/include/ShaderResources.hpp | 2 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 3 + .../Graphics/src/GuestTextureResource.cpp | 15 +- .../Graphics/src/MultisampleTarget.cpp | 42 ++- .../Graphics/src/ShaderResources.cpp | 9 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 1 + .../tests/GuestTextureResource.cpp | 34 ++ .../prx/libSceAgcDriver/tests/Recorder.cpp | 52 +++ .../execution/SampleMultisampleTarget.cpp | 318 ++++++++++++++++++ .../src/DescriptorBindingBuilder.cpp | 5 +- core/shader/recompiler/Recompiler.hpp | 3 +- 14 files changed, 488 insertions(+), 6 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 51bc1a93474..8d606162062 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1229,6 +1229,8 @@ target_link_libraries(agc_driver_gpu_map_on_fiber_tests PRIVATE libSceFiber) target_include_directories(agc_driver_alpha_to_coverage_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>) +target_include_directories(agc_driver_sample_multisample_target_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>) + if(BUILD_TESTING) add_test(NAME agc_driver_undithered_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --undithered-alpha-to-coverage-state) set_tests_properties(agc_driver_undithered_alpha_to_coverage_state PROPERTIES diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp index 997e9dc7e74..70c7579c002 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp @@ -58,7 +58,8 @@ enum class TextureDimension { k2DArray, kCube, k3D, - k1DArray + k1DArray, + k2DMsaa }; struct GuestTextureResource { @@ -86,6 +87,7 @@ struct GuestTextureResource { bool dccPipeAligned = false; std::uint32_t minLod = 0; std::uint32_t allocatedMipCount = 0; + std::uint32_t samples = 1; }; float EffectiveMinLod(const GuestTextureResource& resource); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index bfcc8fd5347..97980329038 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -6,12 +6,15 @@ #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include <cstddef> #include <cstdint> +#include <memory> #include <optional> #include <string> namespace AgcDriver::Graphics { class StorageTexture; +class Texture; +struct GuestTextureResource; class MultisampledCmask { public: @@ -49,6 +52,7 @@ std::uint64_t MultisampledColorBytes(const ColorTarget& target); bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, VkClearColorValue& clear); bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colorFill, const ColorTarget& use, VkClearColorValue& clear); VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target); +std::shared_ptr<Texture> MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components); void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView); bool MultisampledColorFillAbsorbs(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 1fa042481de..49cf67ddb23 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -204,6 +204,7 @@ class ShaderResources { // ranges are fixed by the build), so a Revalidate's collects on the same worker are memo hits. void PrecollectSurfaces() const; bool Reusable() const { return reusable; } + const std::vector<std::uint64_t>& MultisampleSources() const { return multisampleSources; } static bool NeverReusable(std::span<const CompiledShader> shaders); const std::vector<std::shared_ptr<Texture>>& SampledTextures() const { return textures; } bool ChecksFaults() const { return bda != nullptr; } @@ -436,6 +437,7 @@ class ShaderResources { std::size_t maxDrawSnapshotBytes = MaxDrawSnapshotBytes(); std::unique_ptr<BdaResources> bda; bool usesBda = false; + std::vector<std::uint64_t> multisampleSources; bool usesFaultBuffer = false; VkDescriptorSetLayout _layout = VK_NULL_HANDLE; // Whether the layout is this object's own (no cache) and destroyed with it. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 507e1c603a7..fe205155b0e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2098,6 +2098,9 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& const bool lean = recorded && !drawTransitions; const bool multisampled = std::any_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.multisampled; }); Require(lean || !multisampled, "multisampled draws are only rendered as recorded draws"); + for (const auto& binding : targets) { + if (binding.multisampled) Require(std::find(resources->MultisampleSources().begin(), resources->MultisampleSources().end(), binding.color.surfaceAddress) == resources->MultisampleSources().end(), "sampling a multisampled color target the draw renders to is not implemented"); + } if (!lean && !resolved.moved.empty()) { resolved.resources = std::make_shared<ShaderResources>(context, shaders, state.color, static_cast<std::uint32_t>(state.colors.size()), draw.indexAddress, static_cast<std::size_t>(indexBytes), snapshots); resolved.moved.clear(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 55ecd30e305..db5ba5c6f86 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -44,6 +44,7 @@ TextureDimension resolveDimension(std::uint32_t raw) { case 11: return TextureDimension::kCube; case 12: return TextureDimension::k1DArray; case 13: return TextureDimension::k2DArray; + case 14: return TextureDimension::k2DMsaa; default: throw std::runtime_error("AGC graphics: guest texture descriptor uses an unsupported image type " + std::to_string(raw)); } } @@ -76,7 +77,7 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words) const auto baseArray = (words[4] >> 16u) & 0x1fffu; const auto arrayPitch = (words[5] >> 0u) & 0xfu; - const auto maxMip = (words[5] >> 4u) & 0xfu; + auto maxMip = (words[5] >> 4u) & 0xfu; const auto minLodWarn = (words[5] >> 8u) & 0xfffu; const auto cornerSample = ((words[5] >> 23u) & 0x1u) != 0; const auto mipStatsCntEn = ((words[5] >> 25u) & 0x1u) != 0; @@ -138,6 +139,13 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words) } catch (const std::exception& error) { throw std::runtime_error(std::string(error.what()) + describe()); } + std::uint32_t samples = 1; + if (dimension == TextureDimension::k2DMsaa) { + Require(baseLevel == 0 && lastLevel >= 1u && lastLevel <= 4u && maxMip == lastLevel, "guest multisampled texture descriptor does not hold the same sample count of 2 to 16 in its last level and its last mip, which is not implemented" + describe()); + samples = 1u << lastLevel; + lastLevel = 0; + maxMip = 0; + } // Views may name levels past MAX_MIP (a 512x512 view through 1x1 over a 9-level surface); the // hardware never addresses them, so the view ends at the surface's last level. if (baseLevel <= maxMip) lastLevel = std::min(lastLevel, maxMip); @@ -161,6 +169,9 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words) } Require(depth == 0 && baseArray == 0, "guest 2D texture descriptor has a nonzero depth or base array"); break; + case TextureDimension::k2DMsaa: + Require(depth == 0 && baseArray == 0, "guest multisampled 2D texture descriptor has a nonzero depth or base array"); + break; case TextureDimension::k3D: // word4 DEPTH holds the depth minus one; BASE_ARRAY has no meaning for volumes. Require(baseArray == 0, "guest 3D texture descriptor has a nonzero base array"); @@ -207,6 +218,7 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words) result.dccAlphaOnMsb = dccAlphaPos; result.dccPipeAligned = metaPipeAligned; result.minLod = minLod; + result.samples = samples; if (baseLevel > maxMip) { Require(LevelsFitAllocation(result, lastLevel + 1u), "guest texture descriptor starts past the surface's last mip level at levels that would move the surface's own" + describe()); result.mipCount = lastLevel + 1u; @@ -231,6 +243,7 @@ bool MatchesGuestDimension(ShaderRecompiler::DescriptorImageShape shape, Texture case ShaderRecompiler::DescriptorImageShape::ImageCube: return dimension == TextureDimension::kCube; case ShaderRecompiler::DescriptorImageShape::Image3D: return dimension == TextureDimension::k3D; case ShaderRecompiler::DescriptorImageShape::Image1DArray: return dimension == TextureDimension::k1DArray; + case ShaderRecompiler::DescriptorImageShape::Image2DMultisample: return dimension == TextureDimension::k2DMsaa; } throw std::runtime_error("AGC graphics: MatchesGuestDimension encountered an unknown descriptor image shape"); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 792b1395a02..b53579ba050 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include <algorithm> @@ -12,6 +13,7 @@ #include <bit> #include <cstdio> #include <cstring> +#include <map> #include <memory> #include <mutex> #include <optional> @@ -54,8 +56,10 @@ class MultisampleTarget { VkFormatProperties properties{}; context.formatProperties(context.physical, target.format, &properties); Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0, "multisampled render-target format cannot be a color attachment"); - constexpr VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; VkImageFormatProperties supported{}; + sampleable = (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0 && context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage | VK_IMAGE_USAGE_SAMPLED_BIT, 0, &supported) == VK_SUCCESS && (supported.sampleCounts & samples) != 0; + if (sampleable) usage |= VK_IMAGE_USAGE_SAMPLED_BIT; Check(context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0, &supported), "vkGetPhysicalDeviceImageFormatProperties multisampled target"); Require((supported.sampleCounts & samples) != 0 && target.extent.width <= supported.maxExtent.width && target.extent.height <= supported.maxExtent.height, "multisampled render target exceeds device image limits"); try { @@ -120,6 +124,22 @@ class MultisampleTarget { }); } + std::shared_ptr<Texture> Sampled(VkComponentMapping components) { + Require(sampleable, "the device cannot sample the multisampled color target's format at its sample count"); + Require(cmask.Refusal().empty(), cmask.Refusal()); + Require(cmask.Keys() != DccKeys::Clear0000, "sampling a multisampled color target whose CMASK fast clear no draw has applied is not implemented"); + Require(Recorder::Active() != nullptr, "a multisampled color target is only sampled by recorded draws and dispatches"); + recordCommands(context, [&](VkCommandBuffer commands, Recorder*) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + Recorder::CountBarriers(Recorder::CommandClass::Draw, 1); + }); + const std::array<std::uint32_t, 4> key{static_cast<std::uint32_t>(components.r), static_cast<std::uint32_t>(components.g), static_cast<std::uint32_t>(components.b), static_cast<std::uint32_t>(components.a)}; + if (const auto found = textures.find(key); found != textures.end()) return found->second; + auto texture = std::make_shared<Texture>(context, image, target.format, VK_IMAGE_ASPECT_COLOR_BIT, components); + textures.emplace(key, texture); + return texture; + } + VkRenderPass ResolvePass() { if (resolvePass != VK_NULL_HANDLE) return resolvePass; std::array<VkAttachmentDescription, 2> attachments{}; @@ -164,7 +184,11 @@ class MultisampleTarget { MultisampledColorFill colorFill; private: + bool sampleable = false; + std::map<std::array<std::uint32_t, 4>, std::shared_ptr<Texture>> textures; + void release() noexcept { + textures.clear(); if (resolvePass) context.Function<PFN_vkDestroyRenderPass>("vkDestroyRenderPass")(context.device, resolvePass, nullptr); if (view) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, view, nullptr); if (image) context.Function<PFN_vkDestroyImage>("vkDestroyImage")(context.device, image, nullptr); @@ -380,6 +404,22 @@ VkImageView MultisampleTargetView(const Context& context, const ColorTarget& tar return acquire(context, target).view; } +std::shared_ptr<Texture> MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components) { + std::lock_guard lock(targetsMutex()); + const auto format = ResolveTextureFormat(resource.format); + const MultisampleTarget* other = nullptr; + for (const auto& target : targets()) { + if (target->context.device != context.device || target->target.surfaceAddress != resource.baseAddress) continue; + const auto& rendered = target->target; + if (rendered.format == format && rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0) return target->Sampled(components); + other = target.get(); + } + char text[320]; + if (other != nullptr) std::snprintf(text, sizeof(text), "AGC graphics: sampling multisampled color target 0x%llx (%ux%u, %u samples, vk format %d) as a %ux%u texture of %u samples and guest format %u (vk %d) is not implemented", static_cast<unsigned long long>(resource.baseAddress), other->target.extent.width, other->target.extent.height, other->target.samples, static_cast<int>(other->target.format), resource.width, resource.height, resource.samples, resource.format, static_cast<int>(format)); + else std::snprintf(text, sizeof(text), "AGC graphics: sampling the %ux%u multisampled texture 0x%llx of %u samples, which no draw rendered to, is not implemented", resource.width, resource.height, static_cast<unsigned long long>(resource.baseAddress), resource.samples); + throw std::runtime_error(text); +} + void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView) { std::lock_guard lock(targetsMutex()); auto& target = acquire(context, source); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index f09d2cbcb66..18229ec1f01 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -519,6 +519,11 @@ std::uint64_t sampledBudget(const Context& context, TextureCache& cache) { // `guestBytes` is the surface size when the caller described the surface already (0: described here). std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, VkComponentMapping components, std::uint64_t guestBytes = 0, bool depthCompare = false) { CaptureTrace::Log("sampled-lookup address=%llx width=%u height=%u dcc=%llx", static_cast<unsigned long long>(resource.baseAddress), resource.width, resource.height, static_cast<unsigned long long>(resource.dccAddress)); + if (resource.dimension == TextureDimension::k2DMsaa) { + auto target = MultisampleTargetTexture(context, resource, components); + logLookup({target.get(), resource, 0, DccKeys::Uncompressed, 0, nullptr, true}); + return target; + } RequireNoMultisampleTarget(resource.baseAddress, guestBytes != 0 ? static_cast<std::size_t>(guestBytes) : 1u, "a texture read"); if (auto* recorder = Recorder::Active(); recorder != nullptr) recorder->BoundKeptBytes(); if (auto depth = DepthSurfaceTexture(context, words, resource, components)) { @@ -863,6 +868,7 @@ GuestTextureResource StorageSurface(const Context& context, const GuestTextureRe // `guestBytes` is the surface size when the caller described the surface already (0: described here). std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& viewed, std::uint32_t mip, std::uint64_t guestBytes) { + Require(viewed.dimension != TextureDimension::k2DMsaa, "storage image access to a multisampled texture is not implemented"); RequireNoMultisampleTarget(viewed.baseAddress, guestBytes != 0 ? static_cast<std::size_t>(guestBytes) : 1u, "a storage image access"); if (auto depth = DepthSurfaceStorage(context, words, viewed, mip)) return depth; static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; @@ -1739,7 +1745,7 @@ void ShaderResources::noteReusable() { captureValidation(); reusable = false; directRegions.clear(); - if (guestMemory.HasCopiedWrites() || HoldsLease() || usesBda || (bda != nullptr && LoopGuardEnabled())) return; + if (guestMemory.HasCopiedWrites() || HoldsLease() || usesBda || !multisampleSources.empty() || (bda != nullptr && LoopGuardEnabled())) return; if (std::any_of(textures.begin(), textures.end(), [](const std::shared_ptr<Texture>& texture) { return texture != nullptr && texture->RefreshedPerUse(); })) return; if (TemplateDataRefresh() && std::any_of(allocations.begin(), allocations.end(), [](const Allocation& allocation) { return allocation.buffer != nullptr && !allocation.guest && allocation.size > MaxRefreshBytes; })) return; const auto regions = guestMemory.DirectRegions(); @@ -3349,6 +3355,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind (texture != nullptr ? counters.fastHits : counters.fastMisses).fetch_add(1, std::memory_order_relaxed); } if (texture == nullptr) texture = cachedTexture(context, words, resource, components, guestBytes, depthCompare); + if (resource.dimension == TextureDimension::k2DMsaa) multisampleSources.push_back(resource.baseAddress); bool baseLevel = false; if (element < binding.imageUnnormalized.size() && binding.imageUnnormalized[element]) { const auto range = texture->SampledViewRange(firstLayer); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 0ea0b616198..57c1f860294 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -211,6 +211,7 @@ VkImageViewType ViewTypeFor(TextureDimension dimension, [[maybe_unused]] std::ui case TextureDimension::kCube: return VK_IMAGE_VIEW_TYPE_2D_ARRAY; case TextureDimension::k3D: return VK_IMAGE_VIEW_TYPE_3D; case TextureDimension::k1DArray: return VK_IMAGE_VIEW_TYPE_1D_ARRAY; + case TextureDimension::k2DMsaa: break; } throw std::runtime_error("AGC graphics: Texture encountered an unknown guest texture dimension"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index bd5e36614f3..73608465999 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -244,6 +244,37 @@ void RunGuestTextureResourceTests() { cubeNotMultiple.depth = 4; rejectFields(cubeNotMultiple, "multiple of 6"); + Fields multisampled = base; + multisampled.typeRaw = 14; + multisampled.width = 297; + multisampled.height = 297; + multisampled.tileModeRaw = 0x1b; + multisampled.lastLevel = 2; + multisampled.maxMip = 2; + result = DecodeTextureResource(pack(multisampled)); + Require(result.dimension == TextureDimension::k2DMsaa && result.samples == 4 && result.width == 297 && result.height == 297, "4x multisampled descriptor decoded incorrectly"); + Require(result.mipCount == 1 && result.allocatedMipCount == 1 && result.baseLevel == 0 && result.lastLevel == 0, "a multisampled descriptor's sample count was decoded as mip levels"); + Require(DecodeTextureResource(pack(base)).samples == 1, "a single-sample descriptor decoded with several samples"); + const std::array<std::uint32_t, 8> canvas{0x021f2000u, 0x03800000u, 0x804a004au, 0xe1b20facu, 0x00000000u, 0x00400020u, 0x00000000u, 0x00000000u}; + result = DecodeTextureResource(canvas); + Require(result.dimension == TextureDimension::k2DMsaa && result.samples == 4 && result.baseAddress == 0x21f200000ull && result.width == 297 && result.height == 297 && result.format == 56 && result.tileMode == TextureTileMode::kR64KBX, "the 4x canvas descriptor of PPSA99006 decoded incorrectly"); + Fields fewerFragments = multisampled; + fewerFragments.maxMip = 1; + rejectFields(fewerFragments, "same sample count"); + Fields singleSampleType = multisampled; + singleSampleType.lastLevel = 0; + singleSampleType.maxMip = 0; + rejectFields(singleSampleType, "same sample count"); + Fields offsetLevel = multisampled; + offsetLevel.baseLevel = 1; + rejectFields(offsetLevel, "same sample count"); + Fields multisampledSlice = multisampled; + multisampledSlice.depth = 1; + rejectFields(multisampledSlice, "nonzero depth or base array"); + Fields multisampledArray = multisampled; + multisampledArray.typeRaw = 15; + rejectFields(multisampledArray, "unsupported image type"); + Fields badType = base; badType.typeRaw = 0; rejectFields(badType, "unsupported image type"); @@ -438,6 +469,9 @@ void RunGuestTextureResourceTests() { Require(MatchesGuestDimension(Shape::Image2D, TextureDimension::k2D), "2D shape must match 2D dimension"); Require(!MatchesGuestDimension(Shape::Image2D, TextureDimension::k2DArray), "2D shape must not match 2D array dimension"); Require(MatchesGuestDimension(Shape::Image2DArray, TextureDimension::k2DArray), "2D array shape must match 2D array dimension"); + Require(MatchesGuestDimension(Shape::Image2DMultisample, TextureDimension::k2DMsaa), "multisampled shape must match the multisampled dimension"); + Require(!MatchesGuestDimension(Shape::Image2DMultisample, TextureDimension::k2D), "multisampled shape must not match 2D dimension"); + Require(!MatchesGuestDimension(Shape::Image2D, TextureDimension::k2DMsaa), "2D shape must not match the multisampled dimension"); Require(MatchesGuestDimension(Shape::Image2DArray, TextureDimension::kCube), "a cube must be readable as a 2D array of its faces"); Require(MatchesGuestDimension(Shape::ImageCube, TextureDimension::kCube), "cube shape must match cube dimension"); Require(!MatchesGuestDimension(Shape::ImageCube, TextureDimension::k1D), "cube shape must not match 1D dimension"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 3415b5c76bb..1cc19229ca9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5168,6 +5168,58 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { std::lock_guard gpu(GpuMutex()); ShaderResources resources(context, compute); }, "a texture read", "a texture read"); + const auto multisampledWords = [&](std::uint32_t width, std::uint32_t sampleShift, std::uint64_t base) { + return std::vector<std::uint32_t>{ + static_cast<std::uint32_t>(base >> 8u), + static_cast<std::uint32_t>((base >> 40u) & 0xffu) | (56u << 20u) | (((width - 1u) & 3u) << 30u), + ((width - 1u) >> 2u) | ((side - 1u) << 14u), + 0xfacu | (sampleShift << 16u) | (14u << 28u), + 0u, + sampleShift << 4u, + 0u, + 0u, + }; + }; + const auto sampledProgram = [&](ShaderRecompiler::DescriptorKind kind, std::vector<std::uint32_t> words) { + ShaderRecompiler::RecompileResult result; + auto element = binding; + element.kind = kind; + if (kind == ShaderRecompiler::DescriptorKind::StorageImage) element.binding = ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding; + element.imageShape = ShaderRecompiler::DescriptorImageShape::Image2DMultisample; + element.guestDescriptor = std::move(words); + result.bindings.push_back(std::move(element)); + return result; + }; + const auto refusedSample = [&](ShaderRecompiler::DescriptorKind kind, std::vector<std::uint32_t> words, const std::string& expected, const std::string& what) { + const auto refusedProgram = sampledProgram(kind, std::move(words)); + const CompiledShader refusedCompute{ShaderRecompiler::ShaderStage::Compute, &refusedProgram, 0}; + std::string message; + try { + std::lock_guard gpu(GpuMutex()); + ShaderResources resources(context, refusedCompute); + } catch (const std::exception& error) { + message = error.what(); + } + Require(message.find(expected) != std::string::npos, what + " did not throw: " + message); + }; + { + const auto sampled = sampledProgram(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side, 2u, address)); + const CompiledShader sampledCompute{ShaderRecompiler::ShaderStage::Compute, &sampled, 0}; + std::lock_guard gpu(GpuMutex()); + ShaderResources first(context, sampledCompute); + ShaderResources second(context, sampledCompute); + Require(first.SampledTextures().size() == 1 && first.SampledTextures().front() != nullptr, "the multisampled target was not bound as a sampled texture"); + Require(first.SampledTextures().front()->Layout() == VK_IMAGE_LAYOUT_GENERAL && first.SampledTextures().front()->ViewFormat() == target.format, "the sampled multisampled target is not a view of the target's image in its layout"); + Require(second.SampledTextures().front() == first.SampledTextures().front(), "a second lookup of the multisampled target made another texture"); + Require(first.MultisampleSources() == std::vector<std::uint64_t>{address} && !first.Reusable(), "resources sampling a multisampled target are kept for reuse without the barrier their lookup records"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + refusedSample(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side, 3u, address), "as a 64x64 texture of 8 samples", "sampling a 4x target as an 8x texture"); + refusedSample(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side / 2u, 2u, address), "as a 32x64 texture of 4 samples", "sampling a 4x target as a narrower texture"); + refusedSample(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side, 2u, address + 4u * 65536u), "which no draw rendered to", "sampling a multisampled texture no draw rendered to"); + refusedSample(ShaderRecompiler::DescriptorKind::StorageImage, multisampledWords(side, 2u, address), "storage image access to a multisampled texture", "storage access to a multisampled texture"); refused([&] { AgcDriver::GuestMemory::Read(address + 3u * 65536u + 256u, texels); }, "a CPU read", "a CPU read in the padding of a later sample plane"); AgcDriver::GuestMemory::Read(address + 4u * 65536u, texels); clearTargets(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp new file mode 100644 index 00000000000..7a1fe3b4fed --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -0,0 +1,318 @@ +#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/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include <windows.h> +#endif +#include <algorithm> +#include <array> +#include <bit> +#include <cmath> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <iostream> +#include <span> +#include <string> +#include <vector> + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Side = 16; +constexpr std::uint32_t Samples = 4; +constexpr std::uint32_t SampleShift = 2; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Float16x4Info = 0x0730u; +constexpr std::uint32_t MultisampledAttrib = (SampleShift << 12u) | (SampleShift << 15u); +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t Format16161616Float = 71; +constexpr std::uint32_t Type2DMsaa = 14; +constexpr std::uint32_t TileRenderTarget = 0x1b; +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 32; +constexpr std::uint32_t LoadResult = 12; +constexpr std::array<float, 4> Drawn{0.25f, 0.5f, 0.125f, 1.0f}; +constexpr double EdgeMargin = 1.0 / 32.0; + +alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array<std::uint32_t, 11> PixelCode{ + 0x7e0802ff, 0x3e800000, 0x7e0a02ff, 0x3f000000, 0x7e0c02ff, 0x3e000000, 0x7e0e02ff, 0x3f800000, 0xf800180f, 0x07060504, 0xbf810000, +}; + +alignas(256) constexpr std::array<std::uint32_t, 26> LoadCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xbf8c3f70, 0xf0001f30, 0x00011005, + 0xbf8c3f70, 0xe0701030, 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, 0x80001201, 0xe070103c, + 0x80001301, 0xbf810000, +}; + +constexpr std::array<std::array<float, 4>, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {0.83f, -1.0f, 0.5f, 1.0f}, {-1.0f, 0.71f, 0.5f, 1.0f} +}}; + +alignas(256) std::array<std::uint32_t, Threads * Words> Buffer{}; + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes) { +#ifdef _WIN32 + data = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + data = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, bytes)); +#endif + Require(data != nullptr, "cannot allocate the multisampled color target"); + std::memset(data, 0, bytes); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + VirtualFree(data, 0, MEM_RELEASE); +#else + std::free(data); +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(data); } + std::size_t bytes; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { + AgcDriver::Registers cx; + cx[0x318] = static_cast<std::uint32_t>(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Float16x4Info; + cx[0x31d] = MultisampledAttrib; + cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u); + cx[0x3b8] = TiledAttrib3; + cx[0x390] = static_cast<std::uint32_t>(block.Address() >> 40u); + const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R16G16B16A16_SFLOAT && color.samples == Samples && color.extent.width == Side && color.extent.height == Side, "the target did not decode as a 16x16 RGBA16F surface of 4 samples"); + return color; +} + +AgcDriver::Graphics::State TargetState(const AgcDriver::Graphics::ColorTarget& color) { + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Side, Side}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast<float>(Side), static_cast<float>(Side), -static_cast<float>(Side), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Side, Side}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + state.samples = Samples; + return state; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& state) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector<std::uint32_t> vertexUserData(4, 0u); + const auto address = reinterpret_cast<std::uintptr_t>(Triangle.data()); + const std::array<std::uint32_t, 4> vertexBuffer{static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (16u << 16u), static_cast<std::uint32_t>(Triangle.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(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<std::uint32_t>(vertexResult.pushConstants.size()); + const auto codeAddress = reinterpret_cast<std::uintptr_t>(PixelCode.data()); + const std::span<const std::uint32_t> pixelCode(PixelCode); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, Float32Export}}; + const std::array<ShaderRecompiler::MemoryRegion, 1> 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)); + const ShaderRecompiler::SrtRuntime runtime{}; + const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, fragment); + const auto pixelResult = invocation.Materialize(*invocation.Capture(runtime)); + const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::array<std::uint32_t, 8> TargetDescriptor(const Block& block, std::uint32_t sampleShift) { + const auto address = block.Address(); + return { + static_cast<std::uint32_t>(address >> 8u), + static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (Format16161616Float << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (sampleShift << 16u) | (TileRenderTarget << 20u) | (Type2DMsaa << 28u), + 0u, + sampleShift << 4u, + 0u, + 0u, + }; +} + +ShaderRecompiler::RecompileResult CompileLoad(AgcDriver::VulkanDevice& device, const std::array<std::uint32_t, 8>& texture, std::vector<std::uint32_t>& userData) { + const auto address = reinterpret_cast<std::uintptr_t>(Buffer.data()); + const std::array<std::uint32_t, 4> buffer{static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), static_cast<std::uint32_t>(Buffer.size() * 4u), 0x31016facu}; + userData.assign(16, 0u); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::span<const std::uint32_t> code(LoadCode); + const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +std::vector<std::array<float, 4>> LoadSamples(AgcDriver::VulkanDevice& device, const Block& block) { + std::vector<std::uint32_t> userData; + const auto program = CompileLoad(device, TargetDescriptor(block, SampleShift), userData); + std::vector<std::array<float, 4>> loaded(Side * Side * Samples); + for (std::uint32_t first = 0; first < loaded.size(); first += Threads) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto index = first + tid; + const auto pixel = index / Samples; + const std::array<std::uint32_t, 6> words{0u, 0u, 0u, pixel % Side, pixel / Side, index % Samples}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(LoadCode.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t component = 0; component < 4u; ++component) loaded[first + tid][component] = std::bit_cast<float>(Buffer[tid * Words + LoadResult + component]); + } + } + return loaded; +} + +double EdgeDistance(double x, double y, std::size_t from) { + const auto pixels = [](const std::array<float, 4>& vertex) { + return std::array<double, 2>{(static_cast<double>(vertex[0]) + 1.0) * 0.5 * Side, Side - (static_cast<double>(vertex[1]) + 1.0) * 0.5 * Side}; + }; + const auto a = pixels(Triangle[from]); + const auto b = pixels(Triangle[(from + 1u) % Triangle.size()]); + const auto c = pixels(Triangle[(from + 2u) % Triangle.size()]); + const auto length = std::hypot(b[0] - a[0], b[1] - a[1]); + const auto side = [&](double px, double py) { return ((b[0] - a[0]) * (py - a[1]) - (b[1] - a[1]) * (px - a[0])) / length; }; + return side(c[0], c[1]) < 0 ? -side(x, y) : side(x, y); +} + +void CheckSamples(const AgcDriver::Graphics::State& state, const std::vector<std::array<float, 4>>& loaded) { + const auto locations = AgcDriver::Graphics::SampleLocations(state); + std::array<std::uint32_t, Samples + 1u> pixelsByCoverage{}; + std::array<std::uint32_t, Samples> coveredBySample{}; + std::uint32_t checked = 0; + for (std::uint32_t y = 0; y < Side; ++y) { + for (std::uint32_t x = 0; x < Side; ++x) { + std::uint32_t covered = 0; + for (std::uint32_t sample = 0; sample < Samples; ++sample) { + const auto& texel = loaded[(y * Side + x) * Samples + sample]; + const bool drawn = texel == Drawn; + const bool empty = texel == std::array<float, 4>{}; + char where[160]; + std::snprintf(where, sizeof(where), "sample %u of pixel (%u, %u) loaded (%g, %g, %g, %g)", sample, x, y, texel[0], texel[1], texel[2], texel[3]); + Require(drawn || empty, std::string(where) + ", neither the drawn color nor the cleared target"); + covered += drawn ? 1u : 0u; + coveredBySample[sample] += drawn ? 1u : 0u; + const auto px = x + static_cast<double>(locations[sample].x); + const auto py = y + static_cast<double>(locations[sample].y); + const auto distance = std::min({EdgeDistance(px, py, 0), EdgeDistance(px, py, 1), EdgeDistance(px, py, 2)}); + if (std::fabs(distance) < EdgeMargin) continue; + ++checked; + Require(drawn == (distance > 0), std::string(where) + (distance > 0 ? ", which the triangle covers" : ", which the triangle does not cover")); + } + ++pixelsByCoverage[covered]; + } + } + Require(checked >= Side * Side * Samples * 9u / 10u, "fewer than nine in ten samples are clear of the triangle's edges"); + Require(pixelsByCoverage[0] != 0 && pixelsByCoverage[Samples] != 0, "the triangle covers every pixel or none"); + Require(pixelsByCoverage[1] + pixelsByCoverage[2] + pixelsByCoverage[3] != 0, "no pixel is partly covered, so the samples of a pixel were not told apart"); + Require(std::adjacent_find(coveredBySample.begin(), coveredBySample.end(), std::not_equal_to<>()) != coveredBySample.end(), "every sample index has the same coverage, so the sample index was not told apart"); +} + +void RequireRefused(AgcDriver::VulkanDevice& device, const std::array<std::uint32_t, 8>& texture, const std::string& expected, const std::string& what) { + std::string refusal; + try { + std::vector<std::uint32_t> userData; + const auto program = CompileLoad(device, texture, userData); + Buffer.fill(0u); + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(LoadCode.data())); + device.WaitIdle(); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find(expected) != std::string::npos, what + " was not refused: " + refusal); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Block block(4u * BlockBytes); + Block other(BlockBytes); + const auto color = DecodeTarget(block); + const auto state = TargetState(color); + Draw(*device, state); + CheckSamples(state, LoadSamples(*device, block)); + RequireRefused(*device, TargetDescriptor(block, 3u), "texture of 8 samples", "loading a 4x target as an 8x texture"); + RequireRefused(*device, TargetDescriptor(other, SampleShift), "which no draw rendered to", "loading a multisampled texture no draw rendered to"); + std::puts("every sample of a 4x color target loaded through its multisampled texture descriptor matches the triangle drawn into it"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 6081365137c..dd4a0681abe 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -79,7 +79,10 @@ DescriptorRole RoleFor(DescriptorBindingKind kind) { DescriptorImageShape ImageShapeForResource(const ImageResource& image) { const RdnaImageDimensionInfo& info = RdnaImageDimensionInfoFor(image.dimension); if (info.multisampled != 0u) { - fail("DescriptorBindingBuilder::Populate multisampled image resources have no descriptor image shape"); + if (info.arrayed != 0u) { + fail("DescriptorBindingBuilder::Populate multisampled array image resources have no descriptor image shape"); + } + return DescriptorImageShape::Image2DMultisample; } if (info.spirvDimension == spv::Dim1D) { return info.arrayed != 0u ? DescriptorImageShape::Image1DArray : DescriptorImageShape::Image1D; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index f17e0ad63b4..a836c79be27 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -349,7 +349,8 @@ enum class DescriptorImageShape { Image2DArray, ImageCube, Image3D, - Image1DArray + Image1DArray, + Image2DMultisample }; enum class DescriptorRole { From 6e6334e7e9c09cd515985c02a4c8b7d2fc4349c2 Mon Sep 17 00:00:00 2001 From: Nicolas Abosch <88494836+nicolasabosch@users.noreply.github.com> Date: Tue, 13 Oct 2026 01:18:10 -0300 Subject: [PATCH 41/81] fix(agc): apply a pending color memory fill before sampling a multisampled target A uniform fill of a multisampled target's color memory is held until the next draw. A shader that samples the target first now sees the fill. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../Graphics/src/MultisampleTarget.cpp | 1 + .../tests/execution/SampleMultisampleTarget.cpp | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index b53579ba050..082f40dab11 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -129,6 +129,7 @@ class MultisampleTarget { Require(cmask.Refusal().empty(), cmask.Refusal()); Require(cmask.Keys() != DccKeys::Clear0000, "sampling a multisampled color target whose CMASK fast clear no draw has applied is not implemented"); Require(Recorder::Active() != nullptr, "a multisampled color target is only sampled by recorded draws and dispatches"); + ApplyFastClear(target); recordCommands(context, [&](VkCommandBuffer commands, Recorder*) { RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); Recorder::CountBarriers(Recorder::CommandClass::Draw, 1); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index 7a1fe3b4fed..da0ad032b76 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -3,6 +3,7 @@ #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/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include "prx/libc/include/GuestAllocations.hpp" @@ -51,6 +52,7 @@ constexpr std::uint32_t Words = 32; constexpr std::uint32_t LoadResult = 12; constexpr std::array<float, 4> Drawn{0.25f, 0.5f, 0.125f, 1.0f}; constexpr double EdgeMargin = 1.0 / 32.0; +constexpr std::uint32_t FillOnes = 0x3c003c00u; alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{ 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, @@ -281,6 +283,15 @@ void CheckSamples(const AgcDriver::Graphics::State& state, const std::vector<std Require(std::adjacent_find(coveredBySample.begin(), coveredBySample.end(), std::not_equal_to<>()) != coveredBySample.end(), "every sample index has the same coverage, so the sample index was not told apart"); } +void CheckFilled(const std::vector<std::array<float, 4>>& loaded) { + for (std::size_t index = 0; index < loaded.size(); ++index) { + const auto& texel = loaded[index]; + char where[160]; + std::snprintf(where, sizeof(where), "sample %zu of pixel (%zu, %zu) loaded (%g, %g, %g, %g)", index % Samples, index / Samples % Side, index / Samples / Side, texel[0], texel[1], texel[2], texel[3]); + Require(texel == std::array<float, 4>{1.0f, 1.0f, 1.0f, 1.0f}, std::string(where) + " after a fill of the target's color memory with half-float ones"); + } +} + void RequireRefused(AgcDriver::VulkanDevice& device, const std::array<std::uint32_t, 8>& texture, const std::string& expected, const std::string& what) { std::string refusal; try { @@ -307,9 +318,13 @@ int main() { const auto state = TargetState(color); Draw(*device, state); CheckSamples(state, LoadSamples(*device, block)); + const auto colorBytes = AgcDriver::Graphics::MultisampledColorBytes(color); + Require(colorBytes != 0 && colorBytes <= block.bytes, "the 4x target's color memory does not fit its block"); + AgcDriver::Graphics::NoteColorMetadataFill(block.Address(), static_cast<std::size_t>(colorBytes), FillOnes); + CheckFilled(LoadSamples(*device, block)); RequireRefused(*device, TargetDescriptor(block, 3u), "texture of 8 samples", "loading a 4x target as an 8x texture"); RequireRefused(*device, TargetDescriptor(other, SampleShift), "which no draw rendered to", "loading a multisampled texture no draw rendered to"); - std::puts("every sample of a 4x color target loaded through its multisampled texture descriptor matches the triangle drawn into it"); + std::puts("every sample of a 4x color target loaded through its multisampled texture descriptor matches the triangle drawn into it, then the fill of its color memory"); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From 84f6364944ca938f938b79f29f35271e2c1009de Mon Sep 17 00:00:00 2001 From: Nicolas Abosch <88494836+nicolasabosch@users.noreply.github.com> Date: Tue, 13 Oct 2026 20:14:58 -0300 Subject: [PATCH 42/81] fix(agc): skip retired multisampled targets when sampling Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../libSceAgcDriver/Graphics/src/MultisampleTarget.cpp | 1 + core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 082f40dab11..be3c43dfbdb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -410,6 +410,7 @@ std::shared_ptr<Texture> MultisampleTargetTexture(const Context& context, const const auto format = ResolveTextureFormat(resource.format); const MultisampleTarget* other = nullptr; for (const auto& target : targets()) { + if (!target->active->load(std::memory_order_acquire)) continue; if (target->context.device != context.device || target->target.surfaceAddress != resource.baseAddress) continue; const auto& rendered = target->target; if (rendered.format == format && rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0) return target->Sampled(components); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 1cc19229ca9..bd3e66c3497 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5269,12 +5269,19 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { target.surfaceExtent = target.extent; target.samples = 4; VkImageView previous = VK_NULL_HANDLE; + std::shared_ptr<Texture> previousTexture; for (const bool removeAllocation : {false, true}) { VkImageView view = VK_NULL_HANDLE; + std::shared_ptr<Texture> texture; { + const auto sampled = sampledProgram(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side, 2u, address)); + const CompiledShader sampledCompute{ShaderRecompiler::ShaderStage::Compute, &sampled, 0}; std::lock_guard gpu(GpuMutex()); view = MultisampleTargetView(context, target); Require(view != VK_NULL_HANDLE && view != previous, "a remapped allocation reused its retired multisampled image"); + ShaderResources resources(context, sampledCompute); + texture = resources.SampledTextures().front(); + Require(texture != nullptr && texture != previousTexture, "a remapped allocation sampled its retired multisampled image"); recorder.Submit(); device.WaitQueue(); recorder.Sync(); @@ -5301,7 +5308,9 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { } RequireNoMultisampleTarget(address, bytes, "a texture read"); AgcDriver::GuestMemory::Read(address + 256u, texels); + refusedSample(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side, 2u, address), "which no draw rendered to", "sampling a multisampled target whose allocation was unmapped"); previous = view; + previousTexture = texture; } clearTargets(); std::cout << "Multisampled target guest access tests passed\n"; From 1ef97e517c6360fefc847bb565eaba697d12afb7 Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Thu, 15 Oct 2026 01:46:28 +1300 Subject: [PATCH 43/81] fix(shader): group compute derivatives by four consecutive lanes A compute shader with an LOD query was emitted with DerivativeGroupQuadsKHR, which needs both workgroup dimensions even. A guest workgroup of N x 1 lanes failed SPIR-V validation (VUID-StandaloneSpirv-DerivativeGroupQuadsKHR-10151), which agc_driver_waterfall_images hits on every device. GFX10 forms a compute quad from four consecutive lanes, which is DerivativeGroupLinearKHR. The wave64 path no longer reshapes its workgroup to 2 x N, which only made the 2D quads match consecutive lanes. --- .../SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index f4c1da3f2bf..b499d55a61b 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -137,7 +137,7 @@ void DefineModule(SpirvEmitterState& state) { state.module.EmitCapability(spv::CapabilityGroupNonUniformShuffle); } if (state.requirements.computeDerivatives && StageOf(state) == IrShaderStage::Compute) { - state.module.EmitCapability(spv::CapabilityComputeDerivativeGroupQuadsKHR); + state.module.EmitCapability(spv::CapabilityComputeDerivativeGroupLinearKHR); state.module.EmitExtension("SPV_KHR_compute_shader_derivatives"); } const bool fragmentBarycentric = StageOf(state) == IrShaderStage::Pixel && std::any_of(state.inputs.begin(), state.inputs.end(), [](const SpirvInputBinding& input) { @@ -170,10 +170,6 @@ void DefineModule(SpirvEmitterState& state) { localX = ((localX * localY * localZ + 63u) / 64u) * 32u; localY = 1u; localZ = 1u; - if (state.requirements.computeDerivatives) { - localY = localX / 2u; - localX = 2u; - } } state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeLocalSize, localX, localY, localZ); } @@ -210,7 +206,7 @@ void DefineModule(SpirvEmitterState& state) { } } if (state.requirements.computeDerivatives && StageOf(state) == IrShaderStage::Compute) { - state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDerivativeGroupQuadsKHR); + state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDerivativeGroupLinearKHR); } state.module.AddName(state.mainFunc, "main"); if (state.requirements.functionLds) { From 0b5a4aae32faaaf82dbad1e91ca84cbe7f9a81c6 Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic <gjonn@hotmail.com> Date: Mon, 12 Oct 2026 17:43:43 -0400 Subject: [PATCH 44/81] fix(agc): store small pending storage results at each flip --- .../src/Driver/Packets/PacketMemory.cpp | 6 + .../Graphics/include/Texture.hpp | 2 + .../libSceAgcDriver/Graphics/src/Texture.cpp | 20 +++ .../execution/FlipStoresSmallResults.cpp | 155 ++++++++++++++++++ 4 files changed, 183 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/FlipStoresSmallResults.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp index a034cf91f18..35262385386 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -180,6 +180,12 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue } } + if (header == FlipPacketHeader) { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + static_cast<void>(Graphics::StorageTexture::StoreSmallPending("flip")); + } + static const bool syncFlip = std::getenv("APS5_SYNC_FLIP") != nullptr; const bool drains = drainAll ? ((Pm4::AccessesMemory(header) && opcode != 0x16) || opcode == 0x42 || opcode == 0x46 || opcode == 0x58 || header == FlipPacketHeader) : (!wroteOnGpu && !orderedAlready && (opcode == 0x49 || opcode == 0x37 || opcode == 0x40 || opcode == 0x45 || opcode == 0x50 || opcode == 0x83 || sampleDump || (drawPacket && drawDrain) || (header == FlipPacketHeader && syncFlip))); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index f9a0bdd6b69..046fd456a4b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -194,6 +194,8 @@ class StorageTexture : public std::enable_shared_from_this<StorageTexture> { static bool FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except = nullptr, const char* reason = "memory access", PublishScope scope = PublishScope::Whole, bool* published = nullptr); static void FlushAllPending(const char* reason); static void SaveBeforeUnwatch(std::uint64_t address, std::size_t bytes); + static constexpr std::uint64_t SmallStoreBytes = 64 * 1024; + static std::size_t StoreSmallPending(const char* reason); static bool AnyPending(); static void FlushDumpSelected(const char* reason); static bool StoreAtFlipRequested(const char* value); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index a57623bdb1c..eac3ef544b3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3323,6 +3323,26 @@ void StorageTexture::FlushAllPending(const char* reason) { static_cast<void>(FlushPending(0, std::numeric_limits<std::size_t>::max(), nullptr, reason)); } +std::size_t StorageTexture::StoreSmallPending(const char* reason) { + std::vector<std::shared_ptr<StorageTexture>> small; + { + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + for (auto* texture : pending.textures) { + if (texture->guestBytes > SmallStoreBytes) continue; + if (auto alive = texture->weak_from_this().lock()) small.push_back(std::move(alive)); + } + } + std::size_t stored = 0; + for (const auto& texture : small) { + const auto address = texture->descriptor.baseAddress; + const auto bytes = static_cast<std::size_t>(texture->guestBytes); + if (!HostImportCovers(texture->context, address, bytes)) continue; + if (FlushPending(address, bytes, nullptr, reason)) ++stored; + } + return stored; +} + bool StorageTexture::AnyPending() { auto& pending = Pending(); std::lock_guard lock(pending.mutex); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FlipStoresSmallResults.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FlipStoresSmallResults.cpp new file mode 100644 index 00000000000..bbac636c998 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FlipStoresSmallResults.cpp @@ -0,0 +1,155 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "VulkanTestDevice.hpp" +#include <algorithm> +#include <array> +#include <atomic> +#include <chrono> +#include <cstdint> +#include <cstdio> +#include <iostream> +#include <memory> +#include <span> +#include <thread> +#include <vector> + +namespace { + +using AgcDriver::Graphics::Require; +constexpr std::uint32_t Threads = 8; +constexpr std::uint32_t Side = 16; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t OutputHandle = 7; +constexpr std::size_t BlockBytes = 0x20000; +constexpr std::uint32_t Untouched = 0xdeadbeefu; +constexpr std::uint32_t Written = 0x12345678u; + +alignas(256) constexpr std::array<std::uint32_t, 7> StoreCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, Written, 0xf0200108, 0x00020102, 0xbf810000, +}; + +alignas(4) std::atomic<std::uint32_t> Gate = 0; + +std::uint64_t AddressOf(const void* data) { + return static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(data)); +} + +std::uint32_t Low(const void* data) { + return static_cast<std::uint32_t>(AddressOf(data)); +} + +std::uint32_t High(const void* data) { + return static_cast<std::uint32_t>(AddressOf(data) >> 32u); +} + +std::array<std::uint32_t, 8> TextureDescriptor(const std::uint32_t* texels) { + const auto address = AddressOf(texels); + return { + static_cast<std::uint32_t>(address >> 8u), + static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (TileR64KBX << 20u) | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +std::vector<std::uint32_t> Frame(const std::uint32_t* texels) { + std::vector<std::uint32_t> userData(16, 0u); + const auto texture = TextureDescriptor(texels); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::vector<std::uint32_t> words; + const auto registers = [&](const std::uint32_t first, const std::span<const std::uint32_t> values) { + words.push_back(0xc0007600u | (static_cast<std::uint32_t>(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array<std::uint32_t, 3> shape{Threads, 1, 1}; + registers(0x207, shape); + const std::array<std::uint32_t, 2> program{static_cast<std::uint32_t>(AddressOf(StoreCode.data()) >> 8u), static_cast<std::uint32_t>(AddressOf(StoreCode.data()) >> 40u)}; + registers(0x20c, program); + const std::array<std::uint32_t, 1> resource{static_cast<std::uint32_t>(userData.size()) << 1u}; + registers(0x213, resource); + registers(0x240, userData); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x41u}); + words.insert(words.end(), {AgcDriver::FlipPacketHeader, OutputHandle, 0xfffffffeu, 1, 0, 0}); + words.insert(words.end(), {0xc0053c00u, 0x13u, Low(&Gate), High(&Gate), 1u, 0xffffffffu, 0x19u}); + return words; +} + +class Request final : public AgcDriver::IFlipRequest { +public: + explicit Request(std::atomic<int>& ready) : ready(ready) {} + void GpuReady(const std::shared_ptr<AgcDriver::FrameTiming>&) override { ++ready; } + void Fail(std::exception_ptr) noexcept override {} +private: + std::atomic<int>& ready; +}; + +class Output final : public AgcDriver::IVideoOutput { +public: + std::shared_ptr<AgcDriver::IFlipRequest> Reserve(const AgcDriver::FlipInfo&) override { return std::make_shared<Request>(ready); } + void Fail(std::exception_ptr) noexcept override {} + std::atomic<int> ready = 0; +}; + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + std::vector<std::uint32_t> storage((BlockBytes + 0x10000u) / 4u); + auto* texels = reinterpret_cast<std::uint32_t*>((reinterpret_cast<std::uintptr_t>(storage.data()) + 0xffffu) & ~std::uintptr_t{0xffffu}); + std::fill(texels, texels + BlockBytes / 4u, Untouched); + { + GuestAllocations::Mutation mutation; + mutation.Add(texels, BlockBytes, true, true, true); + } + const auto bytes = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(texels))).guestBytes; + Require(bytes <= AgcDriver::Graphics::StorageTexture::SmallStoreBytes, "the test image is larger than an image the flip stores"); + const volatile std::uint32_t* cpu = texels; + const auto stored = [&] { + for (std::size_t index = 0; index < bytes / 4u; ++index) { + if (cpu[index] == Written) return true; + } + return false; + }; + auto output = std::make_shared<Output>(); + AgcDriverRegisterVideoOutput_nid_postfix(OutputHandle, output); + + auto words = Frame(texels); + Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while ((output->ready.load() == 0 || !stored()) && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + const bool flipped = output->ready.load() == 1; + const bool seen = stored(); + Gate.store(1, std::memory_order_release); + + AgcDriverWaitIdle_nid_postfix(); + AgcDriverUnregisterVideoOutput_nid_postfix(OutputHandle, output); + AgcDriverShutdown_nid_postfix(); + { + GuestAllocations::Mutation mutation; + mutation.Remove(texels); + } + Require(flipped, "the flip did not reach the GPU"); + Require(seen, "a small image written before a flip did not reach guest memory while the queue kept working"); + std::puts("flip stores small results tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 62eff52c45c49c06a200602041d06a8b14d01006 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Tue, 13 Oct 2026 18:04:23 +0200 Subject: [PATCH 45/81] feat(agc): sample depth plane arrays from a later base slice A T# reading a depth surface's planes as a 2D array whose BASE_ARRAY is past slice 0 views the plane copy from that slice (the copy holds slices 0 to LAST_ARRAY), instead of being refused. A 2D binding of such a plane array reads its base slice through a one-slice view. Resident Evil Village (PPSA01556: a D32 array read as R32F from slice 1) and Diablo II: Resurrected (PPSA03912: a D16 array read as R16_UINT from slice 1) stopped on the refusal. Rebased onto main from EagleBearerYS's adaptation to the current DepthPlaneCopy. Co-Authored-By: EagleBearerYS <75296019+EagleBearerYS@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --- .../prx/libSceAgcDriver/Graphics/include/Texture.hpp | 2 +- .../libSceAgcDriver/Graphics/src/DepthSurface.cpp | 12 ++++++------ .../prx/libSceAgcDriver/Graphics/src/Texture.cpp | 4 ++-- docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index f9a0bdd6b69..76f639cf232 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -61,7 +61,7 @@ class Texture { // compute pass writing it and the next pass sampling it share one image and copy nothing. // CanCopyFrom says whether the two descriptors address the same surface compatibly. Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components); - Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D); + Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D, std::uint32_t baseLayer = 0); struct DepthCopyTag {}; Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag); static VkFormat DepthCopyFormat(VkFormat colorFormat); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 0e0edce3595..c2893c72fe8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -325,7 +325,7 @@ class DepthPlaneCopy { DepthPlaneCopy(const DepthPlaneCopy&) = delete; DepthPlaneCopy& operator=(const DepthPlaneCopy&) = delete; - std::shared_ptr<Texture> Refresh(std::span<const DepthPlaneSlice> slices, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType) { + std::shared_ptr<Texture> Refresh(std::span<const DepthPlaneSlice> slices, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType, std::uint32_t baseLayer) { Require(slices.size() == layers, "depth plane copy slices do not match its layers"); const auto geometry = DescribeSurface(resource); Require(geometry.layers == layers && geometry.sliceLinearBytes == sliceBytes() && !geometry.mips.empty(), "depth plane copy geometry does not match its layers"); @@ -355,10 +355,10 @@ class DepthPlaneCopy { held = current; } if (!copiedLayers.empty() || !uploads.empty()) record(slices, resource, geometry, copiedLayers, uploads, uploadLayers); - const std::array<std::uint32_t, 5> key{static_cast<std::uint32_t>(components.r), static_cast<std::uint32_t>(components.g), static_cast<std::uint32_t>(components.b), static_cast<std::uint32_t>(components.a), static_cast<std::uint32_t>(viewType)}; + const std::array<std::uint32_t, 6> key{static_cast<std::uint32_t>(components.r), static_cast<std::uint32_t>(components.g), static_cast<std::uint32_t>(components.b), static_cast<std::uint32_t>(components.a), static_cast<std::uint32_t>(viewType), baseLayer}; auto& texture = textures[key]; if (texture == nullptr) { - texture = std::make_shared<Texture>(context, image, format, aspect(), components, viewType); + texture = std::make_shared<Texture>(context, image, format, aspect(), components, viewType, baseLayer); texture->MarkRefreshedPerUse(); } return texture; @@ -381,7 +381,7 @@ class DepthPlaneCopy { VkImage image = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; std::unique_ptr<DeviceBuffer> staging; - std::map<std::array<std::uint32_t, 5>, std::shared_ptr<Texture>> textures; + std::map<std::array<std::uint32_t, 6>, std::shared_ptr<Texture>> textures; std::vector<GuestLayer> guestLayers; std::vector<DepthPlaneSlice> surfaceLayers; bool filled = false; @@ -776,7 +776,7 @@ std::shared_ptr<Texture> DepthSurfaceTexture(const Context& context, std::span<c Require(found->hostFormat == base.format, "sampling several slices or an integer view of a depth plane that the device stores in another depth format is not implemented"); const auto copyFormat = integer ? VK_FORMAT_R16_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT; const auto layers = cube ? 6u : layered ? resource.depthOrLastArray + 1u : 1u; - if ((format != copyFormat && !depthBits) || resource.width != base.extent.width || resource.height != base.extent.height || resource.baseLevel != 0 || resource.lastLevel != 0 || resource.baseArray != 0 || (cube && resource.depthOrLastArray != 0 && resource.depthOrLastArray != 5) || (resource.dimension != TextureDimension::k2D && resource.dimension != TextureDimension::k2DArray && !cube)) { + if ((format != copyFormat && !depthBits) || resource.width != base.extent.width || resource.height != base.extent.height || resource.baseLevel != 0 || resource.lastLevel != 0 || (resource.baseArray != 0 && (cube || !layered || resource.baseArray > resource.depthOrLastArray)) || (cube && resource.depthOrLastArray != 0 && resource.depthOrLastArray != 5) || (resource.dimension != TextureDimension::k2D && resource.dimension != TextureDimension::k2DArray && !cube)) { char text[320]; std::snprintf(text, sizeof(text), "AGC graphics: sampling the depth planes of depth surface 0x%llx (%ux%u, vk format %d) as a %ux%u texture of guest format %u (vk %d), dimension %d, %u slices, levels %u-%u, base slice %u is not implemented", static_cast<unsigned long long>(base.address), base.extent.width, base.extent.height, static_cast<int>(base.format), resource.width, resource.height, resource.format, static_cast<int>(format), static_cast<int>(resource.dimension), layers, resource.baseLevel, resource.lastLevel, resource.baseArray); throw std::runtime_error(text); @@ -794,7 +794,7 @@ std::shared_ptr<Texture> DepthSurfaceTexture(const Context& context, std::span<c if (copy == nullptr) { copy = std::make_unique<DepthPlaneCopy>(context, base.extent, imageFormat, layers); } - return copy->Refresh(slices, resource, components, cube || resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D); + return copy->Refresh(slices, resource, components, cube || resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D, layered && !cube ? resource.baseArray : 0u); } bool DepthSurfaceHolds(const Context& context, const GuestTextureResource& resource, const Texture* texture) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index a57623bdb1c..e6848ac86cf 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -779,7 +779,7 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>& } } -Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType) : context(context) { +Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType, std::uint32_t baseLayer) : context(context) { layout = VK_IMAGE_LAYOUT_GENERAL; VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = depthImage; @@ -787,7 +787,7 @@ Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthForma viewInfo.format = depthFormat; viewFormat = depthFormat; viewInfo.components = components; - viewInfo.subresourceRange = {aspect, 0, 1, 0, viewType == VK_IMAGE_VIEW_TYPE_2D ? 1u : VK_REMAINING_ARRAY_LAYERS}; + viewInfo.subresourceRange = {aspect, 0, 1, baseLayer, viewType == VK_IMAGE_VIEW_TYPE_2D ? 1u : VK_REMAINING_ARRAY_LAYERS}; Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth plane"); viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; if (viewType != VK_IMAGE_VIEW_TYPE_2D_ARRAY) return; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 34828a11a5a..664dc400425 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -34,7 +34,7 @@ - [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane from slice 0 is a storage image over all its slices (`LayeredDepthStorage`): a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, or a sampled copy) is a separate image, so their results are not merged. A matching view still throws when it is a cube, a 2D array of more than one layer of the stencil plane or from a later slice, or another level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. - [Collapsed color array views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) preserve the selected slice in the XOR address equation when the view is represented as a shifted 2D surface. Native Vulkan coverage checks R_X draws, array reads, publication and re-upload for slices 1-3; Lost Soul Aside PPSA26358 validates the RGBA16F cube mip chain on Windows/NVIDIA. Other devices and console comparisons remain unverified. Full-array views retain their layer addressing. -- [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array or a cube, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. Planes with mips, a base slice other than 0, a cube with other than 1 or 6 slices, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below). +- [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array or a cube, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. A 2D-array T# whose `BASE_ARRAY` is past slice 0 reads slices `BASE_ARRAY` to `LAST_ARRAY` of the copy, which holds slices 0 to `LAST_ARRAY`, and a 2D binding of it reads slice `BASE_ARRAY`; that follows the colour 2D-array semantics and the titles that hit it (PPSA01556, PPSA03912), and was not measured on hardware for depth surfaces. Planes with mips, a cube or a 2D T# with a base slice other than 0, a `BASE_ARRAY` past `LAST_ARRAY`, a cube with other than 1 or 6 slices, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below). - [SW_64KB_Z_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the texture equations and 64 KiB block geometry of `SW_64KB_Z_X`, whose addresses are checked against [AMD addrlib GFX10](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) in the texture tiling tests. Any 256-byte pipe/bank XOR below the block size is accepted, as for `SW_64KB_R_X` targets and Z_X textures; which of those bits the console honours, and the render results, are not measured. - [SW_64KB_D_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the existing texture block/mip geometry and equations from [AMD addrlib GFX10 non-RB+](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) for 16 pipes and 256-byte interleave. The address-reference tests use legal pipe/bank XOR bits for that configuration. Raw column bits outside the source configuration's pipe/bank mask 0xcf00 throw; their console meaning is unresolved. Console address equations and render results have not been measured. - [Per-face depth bias](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): Vulkan has one depth bias per draw, so a draw that rasterizes both faces with different front and back bias (PA_SU_SC_MODE_CNTL, PA_SU_POLY_OFFSET_FRONT/BACK_*) is recorded twice, front faces with the back faces culled and then back faces with the front faces culled. The vertex stages run twice, so a vertex shader that writes memory writes twice, and all front faces land before all back faces instead of in primitive order, which differs only for blending or stencil updates that depend on the order. From a5904f437ff4d27a6db0db49c10d1b5ad8f1ffec Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Tue, 13 Oct 2026 18:04:24 +0200 Subject: [PATCH 46/81] test(agc): depth plane arrays sampled from a later base slice agc_driver_depth_plane_sampling's refusal case becomes base-slice cases: a D32 2D array from base slice 1 reads its slices 1 and 2, and through a 2D shader image slice 1; a D16 plane array read as R16_UINT from base slice 1 reads slice 1 (the integer path Diablo II takes). Co-Authored-By: EagleBearerYS <75296019+EagleBearerYS@users.noreply.github.com> Co-Authored-By: mrfelek <17274927+mrfelek@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --- .../tests/execution/DepthPlaneSampling.cpp | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp index 2cdf03f8897..05ac1d98b64 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp @@ -303,6 +303,13 @@ void DepthBitsArrayCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std: // A 2D shader image over a 2D-array T# reads the array's first slice: a one-slice array, which samples the depth // image itself, and a 3-slice array, which samples the plane copy. +void DepthBitsBaseSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span<std::uint32_t> output) { + Shape shape{Format16Uint, 256, 128, Type2DArray, 1, Swizzle64KbZX}; + shape.baseArray = 1; + Load(device, block.At(DepthBitsOffset), shape, 0, output); + Expect(output, 0x1234u, "D16 depth-bits array from base slice 1"); +} + void FirstSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span<std::uint32_t> output) { auto* one = block.At(OneSliceOffset); DrawDepth(device, reinterpret_cast<std::uintptr_t>(one), {128, 128}, VK_FORMAT_D32_SFLOAT, DrawnDepth); @@ -316,18 +323,16 @@ void FirstSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::spa Expect(output, Bits(DrawnDepth), "3-slice D32 2D array through a 2D shader image"); } -// A plane array from a later base slice is still refused (its slice offsets are not modeled here). -void RefusalCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span<std::uint32_t> output) { - bool refused = false; - try { - Shape shape{Format32Float, 128, 128, Type2DArray, 2, Swizzle64KbZX}; - shape.baseArray = 1; - Load(device, block.At(ArrayOffset), shape, 0, output); - } catch (const std::runtime_error& error) { - refused = std::string(error.what()).find("sampling the depth planes of depth surface") != std::string::npos; - if (!refused) throw; - } - Require(refused, "a depth plane array from base slice 1 was not refused"); +void BaseSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span<std::uint32_t> output) { + auto* base = block.At(ArrayOffset); + Shape shape{Format32Float, 128, 128, Type2DArray, 2, Swizzle64KbZX}; + shape.baseArray = 1; + Load(device, base, shape, 0, output); + Expect(output, Bits(0.5f), "D32 2D array from base slice 1, slice 0"); + Load(device, base, shape, 1, output); + Expect(output, Bits(0.75f), "D32 2D array from base slice 1, slice 1"); + Load(device, base, shape, 0, output, true); + Expect(output, Bits(0.5f), "D32 2D array from base slice 1 through a 2D shader image"); } } @@ -342,8 +347,9 @@ int main() { CubeCase(*device, block, output); IntegerCase(*device, block, output); DepthBitsArrayCase(*device, block, output); + DepthBitsBaseSliceCase(*device, block, output); FirstSliceCase(*device, block, output); - RefusalCase(*device, block, output); + BaseSliceCase(*device, block, output); std::puts("depth plane sampling tests passed"); return 0; } catch (const std::exception& error) { From f8aa9046d108d5e0e30e0a78425fcdcd46b17488 Mon Sep 17 00:00:00 2001 From: mrfelek <17274927+mrfelek@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:05:43 +0200 Subject: [PATCH 47/81] test(agc): pin the depth plane sampling views that must keep throwing A base slice past the last slice, a cube from a base slice and a 2D view with a base slice are refused by DepthSurfaceTexture. Each case fails when its check is removed. --- .../tests/execution/DepthPlaneSampling.cpp | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp index 05ac1d98b64..7fb674a05e6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp @@ -1,5 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" @@ -26,6 +28,14 @@ #include <string> #include <vector> +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + namespace { using AgcDriver::Graphics::Require; @@ -335,6 +345,24 @@ void BaseSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span Expect(output, Bits(0.5f), "D32 2D array from base slice 1 through a 2D shader image"); } +void ExpectRefusal(AgcDriver::VulkanDevice& device, GuestBlock& block, std::size_t offset, const Shape& shape, std::uint32_t baseArray, const std::string& expected, const std::string& what) { + const auto words = TextureDescriptor(block.At(offset), shape); + auto resource = AgcDriver::Graphics::DecodeTextureResource(words); + resource.baseArray = baseArray; + try { + static_cast<void>(AgcDriver::Graphics::DepthSurfaceTexture(AgcDriver::VulkanDeviceTestAccess::ContextOf(device), words, resource, {})); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find(expected) != std::string::npos, what + " was refused with an unexpected message: " + error.what()); + return; + } + Require(false, what + " was not refused"); +} + +void RefusalCase(AgcDriver::VulkanDevice& device, GuestBlock& block) { + ExpectRefusal(device, block, ArrayOffset, {Format32Float, 128, 128, Type2DArray, 2, Swizzle64KbZX}, 3, "base slice 3 is not implemented", "a depth plane array whose base slice is past its last slice"); + ExpectRefusal(device, block, CubeOffset, {Format32Float, 128, 128, TypeCube, 5, Swizzle64KbZX}, 1, "base slice 1 is not implemented", "a depth plane cube from base slice 1"); + ExpectRefusal(device, block, ArrayOffset, {Format32Float, 128, 128, Type2D, 0, Swizzle64KbZX}, 1, "slice 1 is not implemented", "a 2D depth plane view with a base slice"); +} } int main() { @@ -350,6 +378,7 @@ int main() { DepthBitsBaseSliceCase(*device, block, output); FirstSliceCase(*device, block, output); BaseSliceCase(*device, block, output); + RefusalCase(*device, block); std::puts("depth plane sampling tests passed"); return 0; } catch (const std::exception& error) { From a08310bdc463aa9d66a7f8bac89497c6ab50a8b9 Mon Sep 17 00:00:00 2001 From: p3nicillin <lcmcclellan12@gmail.com> Date: Wed, 14 Oct 2026 14:28:48 +0100 Subject: [PATCH 48/81] feat(libSceAudioOut): open HDMI bitstream ports with sceAudioOutExOpen Surround Sound Studio (PPSA99051) opens an Ex port for IEC 61937 bursts when a clip is played. Follow the disassembly and console log in sainsaji/PS5-Audio-Passthrough-Research: ExOpen refuses modes outside mask 0x61F with 0x80260015 and opens an S16 stereo port of 256 frames at 48 kHz or 1024 frames at 192 kHz; ExConfigureOutput refuses a nonzero first argument with 0x80268009 and returns 0 for the documented modes and targets; ExClose returns 0. The host has no HDMI bitstream output, so an Ex port has no device and drops its bursts at its own rate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAudioOut/src/AudioOut.cpp | 96 ++++++++++++------- .../prx/libSceAudioOut/tests/AudioOutEx.cpp | 66 +++++++++---- docs/dev/debt/libSceAudioOut.md | 2 +- 3 files changed, 115 insertions(+), 49 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index fcd7d359f82..206466563d1 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -23,6 +23,7 @@ static constexpr int PORT_TYPE_BGM = 1; static constexpr int PORT_TYPE_VOICE = 2; static constexpr int PORT_TYPE_PERSONAL = 3; static constexpr int PORT_TYPE_PADSPK = 4; +static constexpr int PORT_TYPE_EX = 6; static constexpr int PORT_TYPE_VIBRATION = 10; static constexpr int PORT_TYPE_AUDIO3D = 126; static constexpr int PORT_TYPE_AUX = 127; @@ -37,6 +38,12 @@ static constexpr std::uint64_t DRAIN_SLEEP_US = 1000; static constexpr int MONITOR_TYPE_HDMI = 1; static constexpr std::uint32_t HDMI_MONITOR_INFO_SIZE = 0x180; static constexpr std::uint32_t EX_MONITOR_INFO_MIN_SIZE = 0xA0; +static constexpr std::uint32_t EX_OPEN_MODE_MAX = 10; +static constexpr std::uint32_t EX_OPEN_MODES = 0x61F; +static constexpr std::uint32_t EX_CONFIGURE_MODE_DEFAULT = 0xFF; +static constexpr std::uint32_t EX_CONFIGURE_TARGET_DEFAULT = 0xFF; +static constexpr int ERROR_EX_INVALID_MODE = -2144993259; +static constexpr int ERROR_EX_INVALID_CONTROL = -2144960503; enum class Format { Unknown, @@ -297,6 +304,34 @@ static Port* getPort(int handle) { return &g_ports[idx]; } +static int allocatePort(int type, std::uint32_t len, std::uint32_t freq, Format format) { + std::lock_guard<std::mutex> lock(g_mutex); + for (int i = 0; i < PORTS_MAX; i++) { + if (!g_ports[i].used) { + Port& port = g_ports[i]; + port.used = true; + port.type = type; + port.samplesNum = len; + port.freq = freq; + port.format = format; + port.channels = channelsForFormat(format); + port.lastDataOutputTime = 0; + port.virtualQueueEnd = sceKernelGetProcessTime(); + port.mixLevel = type == PORT_TYPE_PADSPK ? DEFAULT_PADSPK_MIX_LEVEL : DEFAULT_VOLUME; + for (int c = 0; c < port.channels; c++) { + port.volume[c] = DEFAULT_VOLUME; + } + if (type != PORT_TYPE_VIBRATION && type != PORT_TYPE_EX) { + openDevice(port); + } + static const bool trace = std::getenv("APS5_TRACE_AUDIOOUT") != nullptr; + if (trace) std::fprintf(stderr, "[audioout] port %d: type %d, %u samples at %u Hz, format %d, device %u\n", i + 1, type, len, freq, static_cast<int>(format), static_cast<unsigned>(port.device)); + return i + 1; + } + } + return -2144993275; +} + extern "C" { int APS5_VABI sceAudioOutInit() { @@ -333,31 +368,7 @@ int APS5_VABI sceAudioOutOpen(int userId, int type, int index, std::uint32_t len throw std::runtime_error("sceAudioOutOpen: unknown format param"); } - std::lock_guard<std::mutex> lock(g_mutex); - for (int i = 0; i < PORTS_MAX; i++) { - if (!g_ports[i].used) { - Port& port = g_ports[i]; - port.used = true; - port.type = type; - port.samplesNum = len; - port.freq = freq; - port.format = format; - port.channels = channelsForFormat(format); - port.lastDataOutputTime = 0; - port.virtualQueueEnd = sceKernelGetProcessTime(); - port.mixLevel = type == PORT_TYPE_PADSPK ? DEFAULT_PADSPK_MIX_LEVEL : DEFAULT_VOLUME; - for (int c = 0; c < port.channels; c++) { - port.volume[c] = DEFAULT_VOLUME; - } - if (type != PORT_TYPE_VIBRATION) { - openDevice(port); - } - static const bool trace = std::getenv("APS5_TRACE_AUDIOOUT") != nullptr; - if (trace) std::fprintf(stderr, "[audioout] port %d: type %d, %u samples at %u Hz, format %d, device %u\n", i + 1, type, len, freq, static_cast<int>(format), static_cast<unsigned>(port.device)); - return i + 1; - } - } - return -2144993275; + return allocatePort(type, len, freq, format); } int APS5_VABI sceAudioOutClose(int handle) { @@ -554,13 +565,30 @@ int APS5_VABI sceAudioOutSetMixLevelPadSpk(int handle, int mixLevel) { return 0; } -int APS5_VABI sceAudioOutExClose() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAudioOutExClose(int handle) { + std::lock_guard<std::mutex> outputLock(g_outputMutex); + std::lock_guard<std::mutex> lock(g_mutex); + Port* port = getPort(handle); + if (port == nullptr || port->type != PORT_TYPE_EX) { + throw std::runtime_error("sceAudioOutExClose: handle " + std::to_string(handle) + " is not an open Ex port"); + } + *port = Port{}; return 0; } -int APS5_VABI sceAudioOutExConfigureOutput() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAudioOutExConfigureOutput(int control, std::uint64_t flags, std::uint32_t mode, std::uint32_t target, std::uint64_t option) { + if (control != 0) { + return ERROR_EX_INVALID_CONTROL; + } + if (flags != 0 || option != 0) { + throw std::runtime_error("sceAudioOutExConfigureOutput: flags " + std::to_string(flags) + " and option " + std::to_string(option) + " not supported"); + } + if (mode > EX_OPEN_MODE_MAX && mode != EX_CONFIGURE_MODE_DEFAULT) { + throw std::runtime_error("sceAudioOutExConfigureOutput: mode " + std::to_string(mode) + " not supported"); + } + if ((target < 1 || target > 3) && target != EX_CONFIGURE_TARGET_DEFAULT) { + throw std::runtime_error("sceAudioOutExConfigureOutput: target " + std::to_string(target) + " not supported"); + } return 0; } @@ -578,9 +606,13 @@ int APS5_VABI sceAudioOutExGetMonitorInfo(int type, int index, void* info, std:: return 0; } -int APS5_VABI sceAudioOutExOpen() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAudioOutExOpen(int userId, std::uint32_t mode) { + (void)userId; + if (mode > EX_OPEN_MODE_MAX || ((EX_OPEN_MODES >> mode) & 1) == 0) { + return ERROR_EX_INVALID_MODE; + } + const bool highRate = mode == 3 || mode == 4 || mode == 10; + return allocatePort(PORT_TYPE_EX, highRate ? 1024 : 256, highRate ? 192000 : 48000, Format::S16Stereo); } } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutEx.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutEx.cpp index 22f70c91287..b96f9dd570f 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOutEx.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutEx.cpp @@ -1,32 +1,66 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include <cstdint> #include <cstdio> #include <stdexcept> -#include <string> +#include <vector> extern "C" { -int APS5_VABI sceAudioOutExClose(); -int APS5_VABI sceAudioOutExConfigureOutput(); -int APS5_VABI sceAudioOutExOpen(); +int APS5_VABI sceAudioOutExOpen(int userId, std::uint32_t mode); +int APS5_VABI sceAudioOutExConfigureOutput(int control, std::uint64_t flags, std::uint32_t mode, std::uint32_t target, std::uint64_t option); +int APS5_VABI sceAudioOutExClose(int handle); +int APS5_VABI sceAudioOutOutput(int handle, const void* ptr); } -using ExFunction = int (APS5_VABI *)(); +static constexpr int SYSTEM_USER = 0xFF; +static constexpr int ERROR_EX_INVALID_MODE = static_cast<int>(0x80260015u); +static constexpr int ERROR_EX_INVALID_CONTROL = static_cast<int>(0x80268009u); -static bool ReportsNotImplemented(ExFunction function, const char* name) { +static bool failed = false; + +static void Require(bool condition, const char* what) { + if (!condition) { + std::fprintf(stderr, "failed: %s\n", what); + failed = true; + } +} + +template <typename Call> +static bool Throws(Call call) { try { - function(); - } catch (const std::runtime_error& error) { - if (std::string(error.what()) == std::string(name) + " not implemented") return true; - std::fprintf(stderr, "%s threw: %s\n", name, error.what()); - return false; + call(); + } catch (const std::runtime_error&) { + return true; } - std::fprintf(stderr, "%s returned instead of reporting that it is not implemented\n", name); return false; } int main() { - bool passed = ReportsNotImplemented(sceAudioOutExClose, "sceAudioOutExClose"); - passed = ReportsNotImplemented(sceAudioOutExConfigureOutput, "sceAudioOutExConfigureOutput") && passed; - passed = ReportsNotImplemented(sceAudioOutExOpen, "sceAudioOutExOpen") && passed; - return passed ? 0 : 1; + Require(sceAudioOutExOpen(SYSTEM_USER, 5) == ERROR_EX_INVALID_MODE, "mode 5 (Linear PCM) is refused"); + Require(sceAudioOutExOpen(SYSTEM_USER, 7) == ERROR_EX_INVALID_MODE, "mode 7 is outside mask 0x61F"); + Require(sceAudioOutExOpen(SYSTEM_USER, 11) == ERROR_EX_INVALID_MODE, "mode 11 is above 10"); + + const int ac3 = sceAudioOutExOpen(SYSTEM_USER, 0); + Require(ac3 > 0, "mode 0 opens a port"); + const std::vector<std::int16_t> burst(256 * 2, 0x1234); + Require(sceAudioOutOutput(ac3, burst.data()) == 256, "a mode 0 port takes 256 frames"); + Require(sceAudioOutOutput(ac3, nullptr) == 256, "a mode 0 port drains"); + + const int eac3 = sceAudioOutExOpen(SYSTEM_USER, 4); + Require(eac3 > 0 && eac3 != ac3, "mode 4 opens a second port"); + const std::vector<std::int16_t> wide(1024 * 2, 0); + Require(sceAudioOutOutput(eac3, wide.data()) == 1024, "a mode 4 port takes 1024 frames"); + + Require(sceAudioOutExConfigureOutput(1, 0, 0, 1, 0) == ERROR_EX_INVALID_CONTROL, "a nonzero first argument is refused"); + Require(sceAudioOutExConfigureOutput(0, 0, 0, 1, 0) == 0, "mode 0 to HDMI"); + Require(sceAudioOutExConfigureOutput(0, 0, 0, 0xFF, 0) == 0, "mode 0 to the default target"); + Require(sceAudioOutExConfigureOutput(0, 0, 0xFF, 0xFF, 0) == 0, "the reset"); + Require(Throws([] { sceAudioOutExConfigureOutput(0, 0, 11, 1, 0); }), "an unknown mode throws"); + Require(Throws([] { sceAudioOutExConfigureOutput(0, 0, 0, 4, 0); }), "an unknown target throws"); + Require(Throws([] { sceAudioOutExConfigureOutput(0, 1, 0, 1, 0); }), "nonzero flags throw"); + + Require(sceAudioOutExClose(ac3) == 0, "close the mode 0 port"); + Require(Throws([ac3] { sceAudioOutExClose(ac3); }), "a closed port throws"); + Require(sceAudioOutExClose(eac3) == 0, "close the mode 4 port"); + return failed ? 1 : 0; } diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 017fe07cf44..e364a4dd34d 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -3,7 +3,7 @@ ### Unknown function info - [sceAudioOut2UserGetSupportedAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp) (libSceAudioOut) - the user handle and two uint32 output pointers follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp), not a console measurement. The masks describe this runtime: no context attributes and port attributes PCM (bit 0) and gain (bit 1). Null outputs and unknown or destroyed user handles throw; console validation order and error values are unknown. Users are tracked for this query, but port ownership and busy-user destruction are not implemented. The other PPSA23566 imports remain declared without parameters, with unknown signatures: [sceConvertKeycodeGetCharacterFromKeyboardData](../../../core/libs/prx/libSceConvertKeycode/Export.cpp), [sce::Json::MemAllocator::notifyError](../../../core/libs/prx/libSceJson2/Export.cpp), [sceNpCommerceDialogOpen2](../../../core/libs/prx/libSceNpCommerce/Export.cpp), [sceProprietaryVoiceChatHelper{Initialize,Terminate,SetVoiceChatState,GetVoiceChatUsageState}](../../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp), [sceVoice{GetResourceInfo,ResetPort,EnableChat,DisableChat}](../../../core/libs/prx/libSceVoice/Export.cpp). Names from the shadPS4 aerolib NID list -- [sceAudioOutExClose, sceAudioOutExConfigureOutput, sceAudioOutExOpen](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99505, which fails to load without them; added without an implementation and declared without parameters. PPSA99505 calls them only for its HDMI bitstream output, an option that is off by default, to read the formats the TV or receiver lists, switch the HDMI output mode and open a port for IEC 61937 bursts ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L110-L179)); [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp) also calls `sceAudioOutExOpen`, `sceAudioOutExConfigureOutput` and `sceAudioOutExClose` for HDMI bitstream output. The two declare the arguments of `sceAudioOutExConfigureOutput` differently and neither is confirmed on a console. Audio reaches the host device through SDL as PCM, so there is no encoded HDMI output to switch to +- [sceAudioOutExOpen, sceAudioOutExConfigureOutput, sceAudioOutExClose](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - the HDMI bitstream calls follow [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/docs/FINDINGS.md#L31-L80), which disassembles firmware 12.70's libSceAudioOut and logs the calls on a console. `sceAudioOutExOpen(user, mode)` returns `0x80260015` for a mode above 10 or outside mask `0x61F`; otherwise it opens a port of internal type 6 in S16 stereo, 256 frames at 48000 Hz for modes 0, 1, 2 and 9 and 1024 frames at 192000 Hz for modes 3, 4 and 10. `sceAudioOutExConfigureOutput(control, flags, mode, target, option)` returns `0x80268009` when `control` is not 0, takes modes 0 to 10 and 0xFF and targets 1 to 3 and 0xFF, and returned 0 on the console; `sceAudioOutExClose` returned 0. The argument registers match [Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L26-L28) and PPSA99051's [bitstream_out.cpp](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/sample-app/src/platform/ps5/bitstream_out.cpp#L29-L32). Audio reaches the host through SDL as PCM and the host has no HDMI bitstream output, so an Ex port has no host device: `sceAudioOutOutput` paces it at its own rate and drops its IEC 61937 bursts, and `sceAudioOutExConfigureOutput` changes nothing. Nonzero `flags` or `option`, other modes or targets, `sceAudioOutExClose` of a handle that is not an Ex port, and `sceAudioOutGetPortState` on an Ex port throw, since the console's answers are unknown - [sceAudioOutExGetMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) reports a sink without audio descriptors: it zeroes the buffer and returns 0, as `sceAudioOutSysGetHdmiMonitorInfo` does, since audio reaches the host through SDL as PCM and there is no EDID to describe. The arguments `(0, 0, info, size)` and the descriptor count as a 32-bit value at +0x98, with 8-byte descriptors from +0xA0, follow [Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L62-L117) (PPSA99505), which passes a 0x400-byte buffer and read that layout from one TV; with no descriptor listed it decodes the audio itself instead of opening a bitstream port. The meaning of the first two arguments and the structure's size are unknown: nonzero first or second arguments and sizes below 0xA0, which cannot hold the count, throw, and every byte of the declared size is written as 0. A null buffer returns `0x80260004` like the other AudioOut calls; the console's code is not measured - [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - assumed to take `(user_id, latency)`: KytyPS5 reads `(user_id, output, latency_us)`, but the known calls set just edi and esi; only `(0xFF, 2)` (PPSA26344) and `(0xFF, 1)` (PPSA14632) are accepted - [sceAudioOut2MasteringInit](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - flag values unknown; only 0 is accepted. Whether the console reports an error for a repeated `sceAudioOut2MasteringInit` or for `sceAudioOut2MasteringTerm` without a matching initialization is unknown; both return 0. PPSA01289 creates the audio device of its next world before it destroys the previous one, so it calls Init twice before Term twice, and only logs a non-zero result From 4a71c6b05aaac6c5a43b215aaa80ef2e0c0002ce Mon Sep 17 00:00:00 2001 From: Josip Parlov <josipparlov@protonmail.com> Date: Tue, 13 Oct 2026 14:18:44 +0200 Subject: [PATCH 49/81] docs(usage): note the NVIDIA 610.47 shader compiler crash on Windows --- docs/user/USAGE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 1e736e2820f..00a6358157d 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -161,6 +161,8 @@ $env:VK_LOADER_DRIVERS_DISABLE = '*dzn*' Do not disable Dozen if it is the only Vulkan driver available; the application would have no device to use. `VK_LOADER_DRIVERS_DISABLE` matches Vulkan driver manifest filenames and requires a Vulkan loader built with headers 1.3.234 or later ([loader documentation](https://github.com/KhronosGroup/Vulkan-Loader/blob/main/docs/LoaderInterfaceArchitecture.md#filter-environment-variables)). +On Windows, NVIDIA driver 610.47 can crash in its shader compiler while pipelines are created: the log ends in `FATAL: unhandled exception 0xc0000005` with `nvgpucomp64.dll` in `rip`, on a driver thread. PPSA25344 on an RTX 3060 stopped this way in 3 of 3 New Game loads, also with `APS5_NO_PIPELINE_LTO=1`; with driver 617.42 the same build and scene ran for 40 minutes. Update the driver when the crash is in `nvgpucomp64.dll`. + ### HDR output HDR output is available on Windows only. Titles see it when the monitor showing the window runs in HDR mode (Settings > Display > Use HDR), the window's Vulkan surface offers an HDR10 (`VK_COLOR_SPACE_HDR10_ST2084_EXT`) swapchain in a 10-bit format, the instance has `VK_EXT_swapchain_colorspace` and the device `VK_EXT_hdr_metadata`. The monitor's luminance comes from DXGI; a monitor that reports no peak luminance is taken as 1000 nits peak and full-frame, 0 nits black. On Linux and other hosts the output is always SDR, because the surface's formats do not show whether the display is in HDR. A title's BT.2100 PQ display buffers then reach the display unchanged; without HDR output they are converted to SDR. From bb4ec482d2868af3f6bbe3ecd9eee03b10e9cbd2 Mon Sep 17 00:00:00 2001 From: eljackjoestar-cmd <eljackjoestar@gmail.com> Date: Tue, 13 Oct 2026 18:15:15 +0400 Subject: [PATCH 50/81] fix(audio): preserve completed AudioOut2 grains across threads --- .../libSceAudioOut/src/AudioOut2Context.cpp | 37 ++++++--- .../libSceAudioOut/src/AudioOut2Internal.hpp | 9 ++- .../prx/libSceAudioOut/src/AudioOut2Port.cpp | 31 +++++--- .../tests/AudioOut2PortBuffers.cpp | 79 ++++++++++++++++++- docs/dev/debt/libSceAudioOut.md | 4 +- 5 files changed, 137 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp index 96ca5c4273a..081850a76cf 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp @@ -17,17 +17,11 @@ #include "prx/libSceAudioOut/include/AudioOutSdl.hpp" #include "prx/libScePad/include/PadState.hpp" -// An AudioOut2 context is the hardware output queue: every push appends a grain (num_grains samples) -// to a queue of queue_depth grains that plays in real time. Titles pace their mixer on that queue, so -// the level must follow the output clock. With an SDL device open, a grain's port buffers are read at -// the next push, the device callback plays the mixed grains behind a cushion and the level counts the -// grains beyond that cushion; otherwise each push is mixed at once and a wall-clock model of the -// queue stands in. - using Clock = std::chrono::steady_clock; static constexpr std::size_t CONTEXT_MEMORY = 0x10000; static constexpr std::uint32_t DEFAULT_MAX_PORTS = 16; +static constexpr std::size_t ADVANCED_GRAINS_MAX = 16; // Pushes, polls and advances traced individually before the trace falls back to the per-second summary. static constexpr std::uint64_t CALL_TRACE_FULL = 16; // Grains the device waits for before it starts playing (and again whenever it ran dry), so the @@ -262,12 +256,24 @@ static void TraceSummary(AudioOut2Context& context, Clock::time_point now) { extern "C" { -// The title's per-tick step between setting the ports' data and pushing. A push records the buffers the -// ports point at and the next push reads them, and the push paces the clock, so nothing is due here. int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle ctx) { auto* context = FromHandle(ctx); if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; std::lock_guard lock(context->lock); + if (context->device != 0) { + if (context->advancedGrains.size() >= ADVANCED_GRAINS_MAX) throw std::runtime_error("AudioOut2: more than 16 completed grains await a push"); + if (!context->advancedMode) { + while (!context->pendingGrains.empty()) { + Render(*context, context->pendingGrains.front()); + context->output.insert(context->output.end(), context->mix.begin(), context->mix.end()); + context->pendingGrains.pop_front(); + context->pendingSince.pop_front(); + } + context->advancedMode = true; + } + context->advancedGrains.push_back(AudioOut2CaptureGrain(*context, true)); + context->grainReady.notify_one(); + } context->advances++; context->summaryAdvances++; if (context->advances <= CALL_TRACE_FULL) AUDIOOUT2_TRACE("t=%.3f Advance ctx %p (pushes so far %llu)\n", AudioOut2TraceSeconds(), static_cast<void*>(context), static_cast<unsigned long long>(context->pushes)); @@ -336,13 +342,24 @@ int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t block lock.lock(); now = Clock::now(); } + if (context->advancedMode && context->advancedGrains.empty()) { + if (!blocking) throw std::runtime_error("AudioOut2: a non-blocking push without a rendered grain is not implemented"); + context->grainReady.wait(lock, [&] { return !context->advancedGrains.empty(); }); + now = Clock::now(); + Drain(*context, now); + } context->queued++; context->playHead += GrainDuration(*context); UpdatePadDevice(*context, now); - auto grain = AudioOut2CaptureGrain(*context); + auto grain = context->advancedMode ? std::move(context->advancedGrains.front()) : AudioOut2CaptureGrain(*context); + if (context->advancedMode) context->advancedGrains.pop_front(); const auto mixed = static_cast<std::uint32_t>(grain.size()); if (context->device == 0) { Render(*context, grain); + } else if (context->advancedMode) { + Render(*context, grain); + if (PendingMs(*context) > MAX_QUEUED_MS) context->dropped++; + else context->output.insert(context->output.end(), context->mix.begin(), context->mix.end()); } else { context->pendingGrains.push_back(std::move(grain)); context->pendingSince.push_back(Clock::now()); diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp index 1a6f8cc7e40..8832eb873a3 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp @@ -1,6 +1,8 @@ #ifndef CORE_LIBS_PRX_LIBSCEAUDIOOUT_SRC_AUDIOOUT2INTERNAL_HPP #define CORE_LIBS_PRX_LIBSCEAUDIOOUT_SRC_AUDIOOUT2INTERNAL_HPP +#include <array> +#include <condition_variable> #include <chrono> #include <cstddef> #include <cstdint> @@ -47,6 +49,8 @@ struct AudioOut2CapturedPort { std::size_t index = 0; std::uint64_t generation = 0; const void* data = nullptr; + std::vector<std::byte> pcm; + std::array<float, AUDIO_OUT2_PORT_CHANNELS_MAX> volume{}; }; using AudioOut2Grain = std::vector<AudioOut2CapturedPort>; @@ -86,6 +90,9 @@ struct AudioOut2Context { // Mixed stereo frames the device has not played yet; it plays silence until the cushion is mixed. std::deque<float> output; bool priming = true; + bool advancedMode = false; + std::condition_variable grainReady; + std::deque<AudioOut2Grain> advancedGrains; std::deque<AudioOut2Grain> pendingGrains; std::deque<std::chrono::steady_clock::time_point> pendingSince; SDL_AudioDeviceID padDevice = 0; @@ -112,7 +119,7 @@ struct AudioOut2Context { // Mixes every port of the context that carries PCM data into out (stereo float, frames frames), summing // onto the zeroed buffer. Returns the number of ports mixed. std::uint32_t AudioOut2MixPorts(const AudioOut2Context& context, const AudioOut2Grain& grain, float* out, float* padOut, std::uint32_t frames); -AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context); +AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context, bool snapshot = false); bool AudioOut2HasPadPorts(const AudioOut2Context& context); // Forgets the ports of a context being destroyed. void AudioOut2ReleasePorts(const AudioOut2Context& context); diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp index bbb30e79688..7db93b5b7c5 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp @@ -6,6 +6,7 @@ #include <mutex> #include <stdexcept> #include <string> +#include <utility> #include <vector> #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -74,15 +75,15 @@ static void ReadFrame(const AudioOut2Port& port, const void* data, std::uint32_t } } -static void AccumulatePadPort(const AudioOut2Port& port, const void* data, AudioOut2Route route, float* out, std::uint32_t frames) { +static void AccumulatePadPort(const AudioOut2Port& port, const void* data, AudioOut2Route route, float* out, std::uint32_t frames, const float* volume) { float in[AUDIO_OUT2_PORT_CHANNELS_MAX]; for (std::uint32_t frame = 0; frame < frames; frame++) { ReadFrame(port, data, frame, in); - AudioOut2AccumulatePadFrame(route, in, port.channels, port.volume, out + static_cast<std::size_t>(frame) * AUDIO_OUT2_PAD_CHANNELS); + AudioOut2AccumulatePadFrame(route, in, port.channels, volume, out + static_cast<std::size_t>(frame) * AUDIO_OUT2_PAD_CHANNELS); } } -static void AccumulatePort(const AudioOut2Port& port, const void* data, float* out, std::uint32_t frames) { +static void AccumulatePort(const AudioOut2Port& port, const void* data, float* out, std::uint32_t frames, const float* volume) { const auto& fold = *port.fold; float in[AUDIO_OUT2_PORT_CHANNELS_MAX]; for (std::uint32_t frame = 0; frame < frames; frame++) { @@ -90,7 +91,7 @@ static void AccumulatePort(const AudioOut2Port& port, const void* data, float* o float left = 0.0f; float right = 0.0f; for (std::uint32_t c = 0; c < port.channels; c++) { - const float sample = in[c] * port.volume[c]; + const float sample = in[c] * volume[c]; if (fold.left[c] != 0.0f) left += sample * fold.left[c]; if (fold.right[c] != 0.0f) right += sample * fold.right[c]; } @@ -99,7 +100,7 @@ static void AccumulatePort(const AudioOut2Port& port, const void* data, float* o } } -AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context) { +AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context, bool snapshot) { std::lock_guard lock(g_portsLock); AudioOut2Grain grain; for (std::size_t index = 0; index < g_ports.size(); index++) { @@ -110,7 +111,15 @@ AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context) { data = port.queuedData.front(); port.queuedData.pop_front(); } - if (data != nullptr) grain.push_back({index, port.generation, data}); + if (data != nullptr) { + AudioOut2CapturedPort captured{index, port.generation, data}; + if (snapshot) { + captured.pcm.resize(static_cast<std::size_t>(context.grain) * port.channels * (port.int16 ? sizeof(std::int16_t) : sizeof(float))); + std::memcpy(captured.pcm.data(), data, captured.pcm.size()); + std::copy_n(port.volume, port.channels, captured.volume.begin()); + } + grain.push_back(std::move(captured)); + } } return grain; } @@ -118,13 +127,17 @@ AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context) { std::uint32_t AudioOut2MixPorts(const AudioOut2Context& context, const AudioOut2Grain& grain, float* out, float* padOut, std::uint32_t frames) { std::lock_guard lock(g_portsLock); std::uint32_t mixed = 0; - for (const auto& [index, generation, data] : grain) { + for (const auto& captured : grain) { + const auto index = captured.index; + const auto generation = captured.generation; + const void* data = captured.pcm.empty() ? captured.data : captured.pcm.data(); if (index >= g_ports.size()) continue; const auto& port = g_ports[index]; if (!port.used || port.generation != generation || port.context != &context || port.channels == 0) continue; const auto route = AudioOut2RouteForPort(port.type, port.channels); - if (route == AudioOut2Route::Main || (context.padDevice == 0 && route == AudioOut2Route::PadSpeaker)) AccumulatePort(port, data, out, frames); - else if (padOut != nullptr) AccumulatePadPort(port, data, route, padOut, frames); + const float* volume = captured.pcm.empty() ? port.volume : captured.volume.data(); + if (route == AudioOut2Route::Main || (context.padDevice == 0 && route == AudioOut2Route::PadSpeaker)) AccumulatePort(port, data, out, frames, volume); + else if (padOut != nullptr) AccumulatePadPort(port, data, route, padOut, frames, volume); mixed++; } return mixed; diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp index 54f467b3348..cac79482e34 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp @@ -1,5 +1,7 @@ #include "SceTypes.hpp" +#include <array> +#include <chrono> #include <cmath> #include <cstddef> #include <cstdint> @@ -7,15 +9,18 @@ #include <filesystem> #include <fstream> #include <functional> +#include <future> #include <iterator> #include <random> #include <string> +#include <thread> #include <vector> extern "C" { int APS5_VABI sceAudioOut2ContextResetParam(AudioOut2ContextParam*); int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam*, void*, std::size_t, AudioOut2ContextHandle*); int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle); +int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle); int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle, std::uint32_t); int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle, const AudioOut2PortParam*, AudioOut2PortHandle*); int APS5_VABI sceAudioOut2PortDestroy(AudioOut2PortHandle); @@ -49,7 +54,7 @@ void SetData(AudioOut2PortHandle port, const void* data) { Require(sceAudioOut2PortSetAttributes(port, &attribute, 1) == 0); } -std::vector<float> Play(const std::function<void(AudioOut2ContextHandle, AudioOut2PortHandle)>& feed) { +std::vector<float> Play(const std::function<void(AudioOut2ContextHandle, AudioOut2PortHandle)>& feed, bool advanced = false) { const auto path = std::filesystem::temp_directory_path() / ("anyps5_audio_out2_port_buffers-" + std::to_string(std::random_device{}()) + ".raw"); std::filesystem::remove(path); SetEnvironment("SDL_DISKAUDIOFILE", path.string()); @@ -67,7 +72,13 @@ std::vector<float> Play(const std::function<void(AudioOut2ContextHandle, AudioOu AudioOut2PortHandle port = 0; Require(sceAudioOut2PortCreate(context, &portParams, &port) == 0); feed(context, port); - for (std::uint32_t push = 0; push < silentGrains; push++) Require(sceAudioOut2ContextPush(context, 1) == 0); + for (std::uint32_t push = 0; push < silentGrains; push++) { + if (advanced) { + SetData(port, nullptr); + Require(sceAudioOut2ContextAdvance(context) == 0); + } + Require(sceAudioOut2ContextPush(context, 1) == 0); + } Require(sceAudioOut2PortDestroy(port) == 0); Require(sceAudioOut2ContextDestroy(context) == 0); @@ -160,10 +171,74 @@ void TestRewrittenBufferRepeats() { RequirePlayed(played, {&first, &second}); } +void TestAdvanceRetainsRewrittenBuffer() { + auto buffer = Grain(0.01f); + const auto first = buffer; + const auto second = Grain(0.02f); + const auto third = Grain(0.03f); + const auto played = Play([&](AudioOut2ContextHandle context, AudioOut2PortHandle port) { + SetData(port, buffer.data()); + Require(sceAudioOut2ContextAdvance(context) == 0); + buffer = second; + SetData(port, buffer.data()); + Require(sceAudioOut2ContextAdvance(context) == 0); + buffer = third; + Require(sceAudioOut2ContextPush(context, 1) == 0); + Require(sceAudioOut2ContextPush(context, 1) == 0); + }, true); + RequirePlayed(played, {&first, &second}); +} + +void TestAdvanceRetainsVolume() { + const auto buffer = Grain(0.02f); + auto reduced = buffer; + for (auto& sample : reduced) sample *= 0.5f; + const auto played = Play([&](AudioOut2ContextHandle context, AudioOut2PortHandle port) { + SetData(port, buffer.data()); + Require(sceAudioOut2ContextAdvance(context) == 0); + const std::array<float, 2> volume{0.5f, 0.5f}; + const AudioOut2Attribute attribute{1, 0, volume.data(), sizeof(volume)}; + Require(sceAudioOut2PortSetAttributes(port, &attribute, 1) == 0); + SetData(port, buffer.data()); + Require(sceAudioOut2ContextAdvance(context) == 0); + Require(sceAudioOut2ContextPush(context, 1) == 0); + Require(sceAudioOut2ContextPush(context, 1) == 0); + }, true); + RequirePlayed(played, {&buffer, &reduced}); +} + +void TestPushingThreadWaitsForAdvance() { + const auto first = Grain(0.01f); + const auto second = Grain(0.02f); + const auto played = Play([&](AudioOut2ContextHandle context, AudioOut2PortHandle port) { + SetData(port, first.data()); + Require(sceAudioOut2ContextAdvance(context) == 0); + Require(sceAudioOut2ContextPush(context, 1) == 0); + std::promise<void> entered; + std::promise<int> done; + auto completed = done.get_future(); + std::thread pusher([&] { + entered.set_value(); + done.set_value(sceAudioOut2ContextPush(context, 1)); + }); + entered.get_future().wait(); + const bool waited = completed.wait_for(std::chrono::milliseconds(10)) == std::future_status::timeout; + SetData(port, second.data()); + Require(sceAudioOut2ContextAdvance(context) == 0); + Require(completed.get() == 0); + pusher.join(); + Require(waited); + }, true); + RequirePlayed(played, {&first, &second}); +} + } int main() { SetEnvironment("SDL_AUDIODRIVER", "disk"); + TestAdvanceRetainsRewrittenBuffer(); + TestAdvanceRetainsVolume(); + TestPushingThreadWaitsForAdvance(); TestBuffersSetBeforeOnePushPlayInOrder(); TestBurstThenGapKeepsEveryBuffer(); TestAdvancingPortWithoutNewBufferPlaysSilence(); diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 017fe07cf44..4978f98eabc 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -2,6 +2,8 @@ ### Unknown function info +- [sceAudioOut2ContextAdvance and sceAudioOut2ContextPush](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) - with an SDL device, Advance snapshots each port's PCM and gain; Push consumes completed grains in order and a blocking Push waits for Advance when none is ready. Inferred from PPSA26358, which rewrites the same 512-frame, 12-channel float buffer on a producer thread and pushes on another thread. Console buffer ownership, snapshot timing and empty-queue errors are unverified. More than 16 completed grains awaiting Push and a nonblocking Push without a completed grain throw; the limit and errors are runtime restrictions, not console measurements. Context destruction concurrent with active calls remains unsupported. Without an SDL device the existing wall-clock path remains. + - [sceAudioOut2UserGetSupportedAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp) (libSceAudioOut) - the user handle and two uint32 output pointers follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp), not a console measurement. The masks describe this runtime: no context attributes and port attributes PCM (bit 0) and gain (bit 1). Null outputs and unknown or destroyed user handles throw; console validation order and error values are unknown. Users are tracked for this query, but port ownership and busy-user destruction are not implemented. The other PPSA23566 imports remain declared without parameters, with unknown signatures: [sceConvertKeycodeGetCharacterFromKeyboardData](../../../core/libs/prx/libSceConvertKeycode/Export.cpp), [sce::Json::MemAllocator::notifyError](../../../core/libs/prx/libSceJson2/Export.cpp), [sceNpCommerceDialogOpen2](../../../core/libs/prx/libSceNpCommerce/Export.cpp), [sceProprietaryVoiceChatHelper{Initialize,Terminate,SetVoiceChatState,GetVoiceChatUsageState}](../../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp), [sceVoice{GetResourceInfo,ResetPort,EnableChat,DisableChat}](../../../core/libs/prx/libSceVoice/Export.cpp). Names from the shadPS4 aerolib NID list - [sceAudioOutExClose, sceAudioOutExConfigureOutput, sceAudioOutExOpen](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99505, which fails to load without them; added without an implementation and declared without parameters. PPSA99505 calls them only for its HDMI bitstream output, an option that is off by default, to read the formats the TV or receiver lists, switch the HDMI output mode and open a port for IEC 61937 bursts ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L110-L179)); [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp) also calls `sceAudioOutExOpen`, `sceAudioOutExConfigureOutput` and `sceAudioOutExClose` for HDMI bitstream output. The two declare the arguments of `sceAudioOutExConfigureOutput` differently and neither is confirmed on a console. Audio reaches the host device through SDL as PCM, so there is no encoded HDMI output to switch to - [sceAudioOutExGetMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) reports a sink without audio descriptors: it zeroes the buffer and returns 0, as `sceAudioOutSysGetHdmiMonitorInfo` does, since audio reaches the host through SDL as PCM and there is no EDID to describe. The arguments `(0, 0, info, size)` and the descriptor count as a 32-bit value at +0x98, with 8-byte descriptors from +0xA0, follow [Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L62-L117) (PPSA99505), which passes a 0x400-byte buffer and read that layout from one TV; with no descriptor listed it decodes the audio itself instead of opening a bitstream port. The meaning of the first two arguments and the structure's size are unknown: nonzero first or second arguments and sizes below 0xA0, which cannot hold the count, throw, and every byte of the declared size is written as 0. A null buffer returns `0x80260004` like the other AudioOut calls; the console's code is not measured @@ -21,5 +23,5 @@ - [sceAudioOut2EnableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) takes no arguments and returns 0, as declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L460-L464) ("Allows chat audio alongside the application's own"). The host mixes no chat audio and AudioOut2 has no chat port, so the call only records that chat audio is allowed. Whether the console fails it without a chat session, when repeated or before `sceAudioOut2Initialize` is unknown. [sceAudioOut2DisableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) is the counterpart, imported by PPSA18216: it takes no arguments, returns 0 and records that chat audio is no longer allowed; its signature is not confirmed by a console measurement, and whether the console fails it without a chat session or when repeated is unknown. - [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) and [sceAudioOut2MasteringInit, sceAudioOut2MasteringTerm and sceAudioOut2MasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) have no effect: there is no 3D rendering or mastering stage. -- [AudioOut2 contexts](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device 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. +- [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. - [sceAudioOutSysGetHdmiMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) reports an HDMI sink without a monitor name or audio descriptors: it zeroes the 0x180 bytes and returns 0, since audio reaches the host through SDL as PCM and there is no EDID to describe. The arguments `(type, info, size)`, the required size 0x180 and the fields titles read (monitor name at +0x0B, 13 bytes; descriptor count as a 32-bit value at +0x94; 8-byte descriptors `[coding type, channels, rate mask, 0, extra, 0, 0, 0]` from +0x98) come from [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/c3be9c2afcb8f7838784ead0fe1c12a2ceea965a/docs/FINDINGS.md) (firmware 12.70, one monitor). What a console without an HDMI audio sink returns, an error or no descriptors, is unknown. The other bytes, among them the header at +0x90 (`01 09 4f 00` on that monitor), have no known meaning and are written as 0. No LPCM descriptor is reported for the host device, as the rate mask and extra byte of one are not known. A null buffer returns `0x80260004` like the other AudioOut calls; the console's code is not measured. Monitor type 0x10, which the console also accepts, other types and sizes other than 0x180 throw: their results and error codes are unknown. From 8847086a006dfe7abab3cd6576a5a6f70406aa75 Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Mon, 12 Oct 2026 23:51:55 -0400 Subject: [PATCH 51/81] refactor(recompiler): emit the v_fma_mixlo/hi and v_interp_*_f16 emulation once per module FPInterpolateF16 and FPInterpolateF32 (v_fma_mixlo_f16, v_fma_mixhi_f16, v_interp_p1ll_f16, v_interp_p1lv_f16, v_interp_p2_f16) were lowered by expanding the f64-based Interpolate emulation inline at every use, about 1200 SPIR-V words each, twice per instruction in wave64 shaders. Interpolate now builds one non-inlined helper function per result width and float mode, created on first use, and each use calls it. The function body is the same instruction sequence the inline expansion produced, so results are unchanged; only the module size changes. agc_fma_mix_once checks that two more uses grow a module by less than the first use with its shared function. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 7 +- .../prx/libSceAgcDriver/tests/FmaMixOnce.cpp | 93 +++++++++++++++++++ .../SpirvBackend/SpirvEmitterState.hpp | 1 + .../src/SpirvAlu/SpirvAluEmitterFloat64.cpp | 46 ++++++++- 4 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/FmaMixOnce.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index dca69100435..55ebd5b8fbc 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -814,6 +814,11 @@ target_link_libraries(agc_formatted_buffer_once_tests PRIVATE shader_recompiler_ set_target_properties(agc_formatted_buffer_once_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_formatted_buffer_once_tests) +add_test_executable(agc_fma_mix_once_tests EXCLUDE_FROM_ALL tests/FmaMixOnce.cpp) +target_link_libraries(agc_fma_mix_once_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_fma_mix_once_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_fma_mix_once_tests) + add_test_executable(agc_rect_list_position_invariant_tests EXCLUDE_FROM_ALL tests/RectListPositionInvariant.cpp) target_link_libraries(agc_rect_list_position_invariant_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_rect_list_position_invariant_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -1426,7 +1431,7 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(agc_shader_bindless agc_shader_srt_read_patching agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() - foreach(agcTest IN ITEMS agc_vertex_input_tests agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests}) + foreach(agcTest IN ITEMS agc_vertex_input_tests agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests} agc_fma_mix_once_tests) if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests") continue() endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/FmaMixOnce.cpp b/core/libs/prx/libSceAgcDriver/tests/FmaMixOnce.cpp new file mode 100644 index 00000000000..aa48f4f1e7f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/FmaMixOnce.cpp @@ -0,0 +1,93 @@ +#include "BdaAbi.hpp" +#include "Recompiler.hpp" +#include <array> +#include <cstdio> +#include <exception> +#include <span> +#include <spirv/unified1/spirv.hpp> +#include <stdexcept> +#include <string> +#include <string_view> +#include <vector> + +namespace { + +using namespace ShaderRecompiler; + +void Check(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +SpirvTarget Target() { + static constexpr std::array<std::uint32_t, 5> capabilities{ + spv::CapabilityInt64, spv::CapabilityFloat64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance, + }; + static constexpr std::array<std::string_view, 4> extensions{ + "SPV_EXT_descriptor_indexing", "SPV_KHR_8bit_storage", "SPV_KHR_float_controls", "SPV_KHR_physical_storage_buffer", + }; + return {0x00401000u, 0x00010300u, 32, BdaAbi::Version, capabilities, extensions, false, + {1024, 1024, 64}, 1024, 32768, {}, {}}; +} + +struct Counts { + std::size_t words = 0; + std::size_t functions = 0; +}; + +Counts Count(std::span<const std::uint32_t> words) { + Check(words.size() >= 5 && words[0] == spv::MagicNumber, "fma mix once: missing SPIR-V module"); + Counts counts{words.size()}; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> spv::WordCountShift; + Check(count != 0 && count <= words.size() - offset, "fma mix once: malformed SPIR-V instruction"); + if ((words[offset] & spv::OpCodeMask) == spv::OpFunction) ++counts.functions; + offset += count; + } + return counts; +} + +Counts Compile(std::uint32_t uses) { + std::vector<std::uint32_t> code; + for (std::uint32_t load = 0; load < 3u; ++load) code.insert(code.end(), {0xe0300000u | (load * 4u), 0x80010000u | ((1u + load) << 8u)}); + code.push_back(0xbf8c3f70u); + std::uint32_t result = 1u; + for (std::uint32_t use = 0; use < uses; ++use) { + const std::uint32_t opcode = use % 2u == 0u ? 0x21u : 0x22u; + code.insert(code.end(), {0xcc000000u | (opcode << 16u) | 10u, (256u + result) | ((256u + 2u) << 9u) | ((256u + 3u) << 18u)}); + result = 10u; + } + code.insert(code.end(), {0xe0700000u, 0x80010000u | (result << 8u)}); + code.push_back(0xbf810000u); + const std::span<const std::uint32_t> words(code); + const std::array<MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(words)}}}; + const std::array<std::uint32_t, 8> userData{0u, 0u, 0u, 0u, 0x10000000u, 0u, 0x1000u, 0x31016facu}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), words, 0, {}}; + request.target = Target(); + request.layout = {0, 0, 0, 128}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.context.memory = memory; + request.useCache = false; + return Count(Recompile(request).spirv.Words()); +} + +} + +int main() { + try { + const auto none = Compile(0); + const auto one = Compile(1); + const auto three = Compile(3); + Check(three.words - one.words < one.words - none.words, "fma mix once: 2 more v_fma_mixlo/mixhi_f16 added " + std::to_string(three.words - one.words) + " SPIR-V words, as many as the first one with its shared emulation (" + std::to_string(one.words - none.words) + ")"); + Check(one.functions == none.functions + 1u, "fma mix once: one v_fma_mixlo_f16 emitted " + std::to_string(one.functions - none.functions) + " functions more than none, expected one shared emulation function"); + Check(three.functions == one.functions, "fma mix once: two more v_fma_mixlo/mixhi_f16 in the same float mode emitted " + std::to_string(three.functions - one.functions) + " more functions, expected calls to the shared one"); + std::printf("fma mix once tests passed (%zu words without v_fma_mix, %zu for 1, %zu for 3)\n", none.words, one.words, three.words); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index afd26653ac2..b3aecb00a26 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -126,6 +126,7 @@ struct SpirvEmitterState { bool nativeF16ModesEmitted = false; bool f16DenormPreserveEmitted = false; std::uint32_t dot2F32F16Function = 0; + std::map<std::pair<bool, std::uint32_t>, std::uint32_t> interpolateFunctions; std::array<std::array<std::uint32_t, 2>, 2> bdaDwordReadFunctions {}; std::array<std::array<std::uint32_t, 2>, 2> bdaSpanReadFunctions {}; std::unordered_map<const IrValue*, std::pair<std::uint32_t, std::uint32_t>> bdaGroupedLoads; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp index 241522188cf..592cb6a6d1e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp @@ -5,6 +5,8 @@ #include <initializer_list> #include <iterator> #include <span> +#include <string> +#include <utility> namespace ShaderRecompiler { namespace { @@ -806,12 +808,52 @@ std::uint32_t EmitFPDot2F32F16(SpirvEmitterState& state, std::uint32_t arg0, std }); } +namespace { + +std::uint32_t InterpolateFunction(SpirvEmitterState& state, bool halfResult, std::uint32_t mode) { + const auto key = std::make_pair(halfResult, mode); + if (const auto found = state.interpolateFunctions.find(key); found != state.interpolateFunctions.end()) return found->second; + const auto f32 = TypeF32(state); + const auto id = state.module.AllocateId(); + struct Restore { + SpirvEmitterState& state; + std::uint32_t label; + ~Restore() { + state.module.EndHelperFunction(); + state.currentLabel = label; + } + }; + state.module.BeginHelperFunction(); + const Restore restore{state, state.currentLabel}; + state.module.AddName(id, std::string(halfResult ? "interpolate_f16_mode_" : "interpolate_f32_mode_") + std::to_string(mode)); + state.module.AddFunction(spv::OpFunction, f32, id, spv::FunctionControlDontInlineMask, state.module.Type(spv::OpTypeFunction, f32, f32, f32, f32)); + std::array<std::uint32_t, 3> parameters{}; + for (auto& parameter : parameters) { + parameter = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionParameter, f32, parameter); + } + EmitLabel(state, state.module.AllocateId()); + state.module.AddFunction(spv::OpReturnValue, Interpolate(state, parameters[0], parameters[1], parameters[2], halfResult, mode)); + state.module.AddFunction(spv::OpFunctionEnd); + state.interpolateFunctions.emplace(key, id); + return id; +} + +std::uint32_t CallInterpolate(SpirvEmitterState& state, std::uint32_t delta, std::uint32_t coordinate, std::uint32_t base, bool halfResult, std::uint32_t mode) { + const auto function = InterpolateFunction(state, halfResult, mode); + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, TypeF32(state), result, function, delta, coordinate, base); + return result; +} + +} + std::uint32_t EmitFPInterpolateF32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1, std::uint32_t arg2, const IrValue* mode) { - return Interpolate(state, arg0, arg1, arg2, false, mode->ImmediateU32()); + return CallInterpolate(state, arg0, arg1, arg2, false, mode->ImmediateU32()); } std::uint32_t EmitFPInterpolateF16(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1, std::uint32_t arg2, const IrValue* mode) { - return Interpolate(state, arg0, arg1, arg2, true, mode->ImmediateU32()); + return CallInterpolate(state, arg0, arg1, arg2, true, mode->ImmediateU32()); } std::uint32_t EmitConvertF32F64(SpirvEmitterState& state, std::uint32_t arg0) { From 8029428497783e57378e15eb444dbcbc2c0c55c7 Mon Sep 17 00:00:00 2001 From: SynnZer0 <243459655+SynnZer0@users.noreply.github.com> Date: Sun, 11 Oct 2026 01:58:56 -0400 Subject: [PATCH 52/81] fix(libkernel): keep host standard streams open on guest close Guest descriptors are host C runtime descriptors. Closing descriptor 0, 1 or 2 closed the host process's standard stream, and the runtime then gave that number to the next file opened anywhere in the process, including files libkernel opens itself, such as the APR reader's. The title's next close of the same number closed that file mid-read. close and sceKernelClose now fail with EBADF for descriptors 0 to 2 that were open when libkernel loaded, and leave them open. A standard descriptor that was not open at load can still be opened and closed by the guest. PPSA21703 calls sceKernelClose(0) repeatedly while loading asset bundles, which made APR reads return 0 or a part of the requested bytes and raised UCRT invalid parameter reports from close. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libkernel/CMakeLists.txt | 11 +++++ .../libkernel/File/include/HostStreams.hpp | 10 +++++ .../prx/libkernel/File/src/HostStreams.cpp | 40 +++++++++++++++++++ core/libs/prx/libkernel/File/src/Open.cpp | 2 + core/libs/prx/libkernel/File/src/Stdio.cpp | 2 + core/libs/tests/GuestHostStreams.cpp | 38 ++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 7 files changed, 104 insertions(+) create mode 100644 core/libs/prx/libkernel/File/include/HostStreams.hpp create mode 100644 core/libs/prx/libkernel/File/src/HostStreams.cpp create mode 100644 core/libs/tests/GuestHostStreams.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 3c678d19a7d..a4fb37e3392 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -14,6 +14,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/File/src/OpenFileLimit.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/SharedMemory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/RandomDevice.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/File/src/HostStreams.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/Stdio.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../libc/src/Rename.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Semaphore/src/Semaphore.cpp @@ -1148,6 +1149,16 @@ if(BUILD_TESTING) set_tests_properties(guest_socket_descriptors PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>") endif() + add_test_executable(guest_host_stream_tests ${LIBS_INCLUDE_DIR}/tests/GuestHostStreams.cpp) + target_include_directories(guest_host_stream_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_host_stream_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_host_stream_tests) + add_test(NAME guest_host_streams COMMAND guest_host_stream_tests) + set_tests_properties(guest_host_streams PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_host_streams PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>") + endif() add_test_executable(guest_socket_event_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketEvents.cpp) target_include_directories(guest_socket_event_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_socket_event_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/File/include/HostStreams.hpp b/core/libs/prx/libkernel/File/include/HostStreams.hpp new file mode 100644 index 00000000000..d40bcb1600d --- /dev/null +++ b/core/libs/prx/libkernel/File/include/HostStreams.hpp @@ -0,0 +1,10 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_HOSTSTREAMS_HPP +#define CORE_LIBS_PRX_LIBKERNEL_FILE_HOSTSTREAMS_HPP + +namespace File { + +bool IsHostStream(int fd); + +} + +#endif diff --git a/core/libs/prx/libkernel/File/src/HostStreams.cpp b/core/libs/prx/libkernel/File/src/HostStreams.cpp new file mode 100644 index 00000000000..0dbc0a53c97 --- /dev/null +++ b/core/libs/prx/libkernel/File/src/HostStreams.cpp @@ -0,0 +1,40 @@ +#include "prx/libkernel/File/include/HostStreams.hpp" +#include <array> +#include <cstddef> +#include <cstdint> +#ifdef _WIN32 +#include <io.h> +#include <stdlib.h> +#else +#include <fcntl.h> +#endif + +namespace { + +#ifdef _WIN32 +extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); +void IgnoreInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} +#endif + +bool OpenAtStart(int fd) { +#ifdef _WIN32 + const auto previous = _set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); + const bool open = _get_osfhandle(fd) != -1; + _set_thread_local_invalid_parameter_handler(previous); + return open; +#else + return ::fcntl(fd, F_GETFD) != -1; +#endif +} + +const std::array<bool, 3> g_hostStreams = {OpenAtStart(0), OpenAtStart(1), OpenAtStart(2)}; + +} + +namespace File { + +bool IsHostStream(int fd) { + return fd >= 0 && fd < static_cast<int>(g_hostStreams.size()) && g_hostStreams[static_cast<std::size_t>(fd)]; +} + +} diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 49899033360..0f90fb1694d 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -11,6 +11,7 @@ #include "prx/libkernel/File/include/FileLock.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" +#include "prx/libkernel/File/include/HostStreams.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "SceTypes.hpp" @@ -233,6 +234,7 @@ int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std } int APS5_VABI sceKernelClose(int d) { + if (File::IsHostStream(d)) return SCE_KERNEL_ERROR_EBADF; if (GuestSockets::IsOpen(d)) return GuestSockets::Close(d) == 0 ? 0 : SceKernelError(*__error_nid_postfix()); EqueueDescriptorClosed(d); File::ForgetRandomDevice(d); diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 58f00fafaac..55cfd9736ba 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -16,6 +16,7 @@ #include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libkernel/File/include/FileLock.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" +#include "prx/libkernel/File/include/HostStreams.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" @@ -565,6 +566,7 @@ int APS5_VABI chflags_nid_postfix(const char* path, std::uint64_t flags) { } int APS5_VABI close_nid_postfix(int d) { + if (File::IsHostStream(d)) return PosixFailure(GUEST_EBADF); int socketError = 0; if (KernelCloseSocket_nid_no_patch(d, &socketError)) { if (socketError == 0) return 0; diff --git a/core/libs/tests/GuestHostStreams.cpp b/core/libs/tests/GuestHostStreams.cpp new file mode 100644 index 00000000000..e31c1dad517 --- /dev/null +++ b/core/libs/tests/GuestHostStreams.cpp @@ -0,0 +1,38 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include <cstdlib> + +extern "C" { +int APS5_VABI dup_nid_postfix(int); +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI sceKernelClose(int); +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool value) { if (!value) std::abort(); } + +constexpr int GuestEbadf = 9; +constexpr int SceKernelErrorEbadf = static_cast<int>(0x80020009); + +static bool Open(int fd) { + const int duplicate = dup_nid_postfix(fd); + if (duplicate < 0) return false; + Require(close_nid_postfix(duplicate) == 0); + return true; +} + +int main() { + bool open[3]; + for (int fd = 0; fd < 3; ++fd) open[fd] = Open(fd); + for (int fd = 0; fd < 3; ++fd) { + if (!open[fd]) continue; + Require(close_nid_postfix(fd) == -1 && *__error_nid_postfix() == GuestEbadf); + Require(sceKernelClose(fd) == SceKernelErrorEbadf); + Require(Open(fd)); + } + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + for (const int fd : pipe) Require(fd >= 3 || !open[fd]); + Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0); + return 0; +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6eaefa8c882..48169706cc1 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -99,6 +99,7 @@ - [inet_pton, inet_ntop](../../../core/libs/prx/libkernel/Socket/src/AddressConversion.cpp) (libkernel) - the text forms are assumed to be those of FreeBSD 11.4 and are unverified on a console: `inet_pton` accepts only four decimal parts without leading zeros for `AF_INET` and the RFC 4291 forms for `AF_INET6` (one `::` standing for at least one group, a dotted-quad tail only as the last 32 bits, no zone id), and `inet_ntop` prints lowercase hex with the first longest run of two or more zero groups compressed, and a dotted tail only for `::a.b.c.d` and `::ffff:a.b.c.d`. They no longer depend on the host resolver library - [sceKernelIsThreadSanitizerEnabled](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) (libkernel, NID `LzoM-wVLJDE`) - no source gives the signature; `int (void)` is assumed from `sceKernelIsAddressSanitizerEnabled` and from PPSA23226's bundled `libSceJobManager_nosubmission.prx`, which passes no arguments and tests only the 32-bit result - [getdtablesize](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - throws: FreeBSD returns the smaller of the `RLIMIT_NOFILE` soft limit and `maxfilesperproc` ([`sys_getdtablesize`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L338-L355)), and neither value is known for the console, where `getrlimit(RLIMIT_NOFILE)` throws too. VLC (PPSA85300) imports it for a `closefrom` loop in the child of its spawn helper ([libc_stubs.c](https://github.com/ZiZc3/VLC/blob/d46be6a68f4244466b041c2dc1b640915ac1a983/ps5/compat/libc_stubs.c#L66-L72)), which never runs because its `fork` fails with `ENOSYS` +- [close, sceKernelClose](../../../core/libs/prx/libkernel/File/src/HostStreams.cpp) (libkernel) - descriptors 0 to 2 that are open when libkernel loads belong to the host process: closing one fails with `EBADF` and leaves it open. The console's result is unknown. PPSA21703 calls `sceKernelClose(0)` repeatedly; closing the host stream let the host C runtime give descriptor 0 to the next file opened, which the next call then closed ### Functional From f6e9cd6ea5ca3cab118789856af835a3abb22674 Mon Sep 17 00:00:00 2001 From: hossiendehghan989 <1.86807511e+08+hossiendehghan989@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:02:27 +0000 Subject: [PATCH 53/81] fix(videoout): keep mouse helpers out of NID exports --- core/libs/prx/libSceMouse/Export.cpp | 4 ++-- core/libs/prx/libSceMouse/include/MouseState.hpp | 4 ++-- core/libs/prx/libSceMouse/tests/Mouse.cpp | 2 +- core/libs/prx/libSceVideoOut/src/MouseInput.cpp | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceMouse/Export.cpp b/core/libs/prx/libSceMouse/Export.cpp index f41c9eaf484..0fd4352a9f8 100644 --- a/core/libs/prx/libSceMouse/Export.cpp +++ b/core/libs/prx/libSceMouse/Export.cpp @@ -19,7 +19,7 @@ int APS5_VABI sceMouseRead(std::int32_t handle, MouseData* data, std::int32_t nu return Mouse::Read(handle, data, num); } -void MousePublishInput_nid_postfix(const MouseInputEvent& event) { +void MousePublishInput_nid_no_patch(const MouseInputEvent& event) { Mouse::Publish(event); } @@ -28,7 +28,7 @@ int APS5_VABI sceMouseResetAbsolutePosition() { return 0; } -bool MouseIsOpen_nid_postfix() { +bool MouseIsOpen_nid_no_patch() { return Mouse::IsOpen(); } diff --git a/core/libs/prx/libSceMouse/include/MouseState.hpp b/core/libs/prx/libSceMouse/include/MouseState.hpp index 15c89831b68..5abc3c3e6a7 100644 --- a/core/libs/prx/libSceMouse/include/MouseState.hpp +++ b/core/libs/prx/libSceMouse/include/MouseState.hpp @@ -24,7 +24,7 @@ int Read(std::int32_t handle, MouseData* data, std::int32_t num); void Publish(const MouseInputEvent& event); } -extern "C" void MousePublishInput_nid_postfix(const MouseInputEvent& event); -extern "C" bool MouseIsOpen_nid_postfix(); +extern "C" void MousePublishInput_nid_no_patch(const MouseInputEvent& event); +extern "C" bool MouseIsOpen_nid_no_patch(); #endif diff --git a/core/libs/prx/libSceMouse/tests/Mouse.cpp b/core/libs/prx/libSceMouse/tests/Mouse.cpp index e01f3f9b4f4..2793ec01846 100644 --- a/core/libs/prx/libSceMouse/tests/Mouse.cpp +++ b/core/libs/prx/libSceMouse/tests/Mouse.cpp @@ -92,7 +92,7 @@ int main() { for (int i = 0; i < 70; ++i) { MouseInputEvent motion{}; motion.x = i; - MousePublishInput_nid_postfix(motion); + MousePublishInput_nid_no_patch(motion); } Require(sceMouseRead(MOUSE_HANDLE, data, 64) == 64); Require(data[0].x_axis == 6 && data[63].x_axis == 69); diff --git a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp index c00aef3ff31..42866332902 100644 --- a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp @@ -54,7 +54,7 @@ void MouseInput::HandleEvent(const SDL_Event& event, unsigned windowId) { default: return; } - MousePublishInput_nid_postfix(input); + MousePublishInput_nid_no_patch(input); } void MouseInput::SetPresentation(std::uint32_t sourceWidth, std::uint32_t sourceHeight, std::uint32_t presentedWidth, std::uint32_t presentedHeight) { @@ -78,7 +78,7 @@ std::int32_t MouseInput::scaled(std::int32_t delta, double scale, double& remain } bool MouseInput::CaptureWanted(bool allowed) const { - return allowed && focused && MouseIsOpen_nid_postfix(); + return allowed && focused && MouseIsOpen_nid_no_patch(); } void MouseInput::UpdateCapture(bool allowed) { From c794273ab2fbf2146027728de2accecc49b527be Mon Sep 17 00:00:00 2001 From: Alex Collini <pipodi93@gmail.com> Date: Tue, 13 Oct 2026 11:40:25 +0200 Subject: [PATCH 54/81] feat(agc): implement image tables whose records hold a pointer to the T# A table column can now read its descriptor through a 64-bit pointer stored in each record: the shader loads the record's pointer at key * stride + addend + offset from a scalar buffer, then the T# (or S#) through that pointer. The materializer reads each record's pointer when it builds the table, reads the descriptor behind it and records one read range per record. Pointer columns take their own path ahead of the batched record reader, since a descriptor's address is known only once its record's pointer is read. The column is planned only when the descriptor dwords are read at an immediate offset from a GetAddressResource whose two dwords are consecutive reads of one buffer at the same affine offset. PPSA26344 compute shader 0x8000333c00 picks a record per lane in a waterfall loop, reads the record's pointer from it and samples the image behind it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../tests/execution/ImageTable.cpp | 65 ++++++++++++++++++ .../IrMetadata/ControlFlowInfo.hpp | 5 +- .../Optimization/src/ResourceMaterializer.cpp | 34 +++++++++- .../Optimization/src/ResourceTracker.cpp | 66 +++++++++++++++++++ core/shader/recompiler/ShaderDiskCache.cpp | 12 +++- 5 files changed, 177 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index d5eb6a23e54..4fd08658280 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -237,6 +237,15 @@ alignas(256) constexpr std::array<std::uint32_t, 25> ReadPointerCode{ 0xbf810000u, }; +alignas(256) constexpr std::array<std::uint32_t, 32> IndirectRecordCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, + 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, 0xf4240b02u, 0x24000008u, 0xf40c0916u, 0xfa000020u, + 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0xf09c0f08u, + 0x01090401u, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + +alignas(256) std::array<std::uint32_t, Records * 16> Indirect{}; + constexpr std::array<std::uint32_t, 4> PointClamp{0x92u, (4u * 256u) << 12u, 0u, 0u}; constexpr std::array<std::uint32_t, 4> LinearWrap{0u, (4u * 256u) << 12u, (1u << 20u) | (1u << 22u), 0u}; constexpr std::array<std::uint32_t, 4> PointWrap{0u, (4u * 256u) << 12u, 1u << 24u, 0u}; @@ -407,6 +416,26 @@ Outcome RunPointer(AgcDriver::VulkanDevice& device, const std::array<std::uint32 return Execute(device, span, root, memory, groups, Lanes, 64); } +template <std::size_t CodeWords> +Outcome RunIndirectRecords(AgcDriver::VulkanDevice& device, const std::array<std::uint32_t, CodeWords>& code, std::uint32_t groups) { + const auto table = Buffer(Table.data(), static_cast<std::uint32_t>(sizeof(Table))); + const auto keys = Buffer(Keys.data(), static_cast<std::uint32_t>(sizeof(Keys))); + const auto output = Buffer(Output.data(), static_cast<std::uint32_t>(sizeof(Output))); + std::copy(table.begin(), table.end(), Srt.begin()); + std::copy(keys.begin(), keys.end(), Srt.begin() + 4); + std::copy(output.begin(), output.end(), Srt.begin() + 8); + std::copy(PointClamp.begin(), PointClamp.end(), Srt.begin() + 12); + const std::span<const std::uint32_t> span(code); + const std::array<ShaderRecompiler::MemoryRegion, 5> memory{{ + {reinterpret_cast<std::uintptr_t>(span.data()), std::as_bytes(span)}, + {reinterpret_cast<std::uintptr_t>(Srt.data()), std::as_bytes(std::span(Srt))}, + {reinterpret_cast<std::uintptr_t>(Table.data()), std::as_bytes(std::span(Table))}, + {reinterpret_cast<std::uintptr_t>(Keys.data()), std::as_bytes(std::span(Keys))}, + {reinterpret_cast<std::uintptr_t>(Indirect.data()), std::as_bytes(std::span(Indirect))}, + }}; + return Execute(device, span, Srt.data(), memory, groups, Lanes, 64); +} + void SetEntry(std::uint32_t entry, const std::array<std::uint32_t, 4>& image) { auto* words = Root.data() + (PaletteOffset + entry * 32u) / 4u; std::fill(words, words + 8, 0u); @@ -1035,6 +1064,41 @@ void RunPointerTests(AgcDriver::VulkanDevice& device) { RequireRed(reads, 4, Lanes, [](std::uint32_t x) { return UintTexels[x]; }, "pointer image table read: key 0x1ff masked to entry 255"); } +void RunPointerRecordTests(AgcDriver::VulkanDevice& device) { + Fill(); + const auto floatImage = Image(FloatTexels.data(), Format32Float); + const auto uintImage = Image(UintTexels.data(), Format32UInt); + const auto sintImage = Image(SintTexels.data(), Format32SInt); + const std::array<std::array<std::uint32_t, 4>, 3> images{floatImage, uintImage, sintImage}; + const std::array<std::function<std::uint32_t(std::uint32_t)>, 3> texels{ + [](std::uint32_t x) { return std::bit_cast<std::uint32_t>(FloatTexels[x]); }, + [](std::uint32_t x) { return UintTexels[x]; }, + [](std::uint32_t x) { return static_cast<std::uint32_t>(SintTexels[x]); }, + }; + const std::array<const char*, 3> names{"float", "uint", "sint"}; + + Table = {}; + Indirect = {}; + Keys = {}; + for (std::uint32_t group = 0; group < 3; ++group) Keys[group] = 2u - group; + for (std::uint32_t record = 0; record < 3; ++record) { + const std::uint32_t entry = 2u - record; + std::copy(images[record].begin(), images[record].end(), Indirect.data() + entry * 16u + 8u); + const auto pointer = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(Indirect.data() + entry * 16u)); + Table[record * (Stride / 4u) + 2u] = static_cast<std::uint32_t>(pointer); + Table[record * (Stride / 4u) + 3u] = static_cast<std::uint32_t>(pointer >> 32u); + } + const auto indirect = RunIndirectRecords(device, IndirectRecordCode, 3); + Require(!Faulted(indirect), "image table: a T# behind a pointer in its record faulted:\n" + indirect.log); + Require(indirect.imageTableCount == 3u, "image table: T#s behind pointers in their records do not hold one element per distinct T# (" + std::to_string(indirect.imageTableCount) + ")"); + for (std::uint32_t group = 0; group < 3; ++group) RequireRed(indirect, group, Lanes, texels[2u - group], std::string(names[2u - group]) + " texels of a T# behind a pointer in its record"); + + const auto entry = Indirect.data() + 16u; + std::copy(floatImage.begin(), floatImage.end(), entry + 8u); + const auto changed = RunIndirectRecords(device, IndirectRecordCode, 3); + RequireRed(changed, 1, Lanes, texels[0], "float texels after the T# behind record 1's pointer changed"); +} + void RunPointerStorageWriteTests(AgcDriver::VulkanDevice& device) { alignas(256) static std::array<std::uint32_t, 256u * 8u> records{}; alignas(256) static std::array<std::uint32_t, 256u * 8u> otherRecords{}; @@ -1234,6 +1298,7 @@ int main() { RunModeTests(*device); RunStoreTests(*device); RunPointerTests(*device); + RunPointerRecordTests(*device); RunPointerStorageWriteTests(*device); RunGraphicsTests(*device); std::puts("image table tests passed"); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp index f361365f4b5..dd2c624c312 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp @@ -39,9 +39,12 @@ struct TableColumn { bool address = false; std::vector<KeyDomain> keyDomains; bool unkeyed = false; + bool indirect = false; + std::uint32_t indirectOffset = 0; + std::uint32_t indirectImmediate = 0; bool operator==(const TableColumn& other) const { - return heapSource == other.heapSource && stride == other.stride && addend == other.addend && offset == other.offset && dwordCount == other.dwordCount && sampler == other.sampler && address == other.address; + return heapSource == other.heapSource && stride == other.stride && addend == other.addend && offset == other.offset && dwordCount == other.dwordCount && sampler == other.sampler && address == other.address && indirect == other.indirect && indirectOffset == other.indirectOffset && indirectImmediate == other.indirectImmediate; } }; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index e6387263976..4ecd7e90f2f 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -555,7 +555,7 @@ class TableSnapshotter { } records.base = heap.Base48(); records.size = heap.GetSize(); - records.records = ImageTableAbi::ScalarBufferRecords(column.addend, column.stride, column.offset, records.size); + records.records = ImageTableAbi::ScalarBufferRecords(column.addend, column.stride, column.offset + (column.indirect ? 4u : 0u), records.size); if (records.size != 0u) tables.ranges.emplace_back(records.base, records.size); } records.records = std::min(records.records, ImageTableAbi::KeyRecords(column.maxKey, column.stride)); @@ -567,6 +567,38 @@ class TableSnapshotter { } records.words.assign(records.keys, 0u); const auto end = records.base + records.size; + if (column.indirect) { + for (std::uint32_t record = 0; record < records.keys; record++) { + if (records.states[record] != RecordState::Read) continue; + const auto offset = column.addend + record * column.stride; + const auto lowWord = ImageTableAbi::ScalarBufferDword(offset, column.offset, records.size); + const auto highWord = ImageTableAbi::ScalarBufferDword(offset, column.offset + 4u, records.size); + if (!lowWord.has_value() || !highWord.has_value() || !PageAccessible(records.base + *lowWord, records.base, end) || !PageAccessible(records.base + *highWord, records.base, end)) { + records.states[record] = RecordState::Unmapped; + continue; + } + const std::uint64_t pointer = (static_cast<std::uint64_t>(Read(records.base + *highWord)) << 32u) | Read(records.base + *lowWord); + DescriptorValue value; + value.dwordCount = column.sampler ? 4u : 8u; + std::uint64_t first = std::numeric_limits<std::uint64_t>::max(); + std::uint64_t past = 0; + for (std::uint32_t dword = 0; dword < column.dwordCount; dword++) { + auto address = ImageTableAbi::ScalarAddressDword(pointer, column.indirectOffset, column.indirectImmediate + dword * 4u); + if (address.has_value() && (*address & 3u) != 0u) address.reset(); + if (!address.has_value() || !AddressAccessible(*address)) { + records.states[record] = RecordState::Unmapped; + break; + } + value.dwords[dword] = Read(*address); + first = std::min(first, *address); + past = std::max(past, *address + sizeof(std::uint32_t)); + } + if (records.states[record] != RecordState::Read) continue; + records.words[record] = WordsIndex(value); + tables.ranges.emplace_back(first, past - first); + } + return records; + } constexpr auto Unread = std::numeric_limits<std::uint32_t>::max(); std::vector<std::uint32_t> whole(records.keys, Unread); std::vector<std::pair<std::uint32_t, std::array<std::uint32_t, 8>>> split; diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 5209bd0cfba..11e737fa973 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -599,6 +599,10 @@ class Tracker { std::uint32_t stride = 0; std::uint32_t addend = 0; bool masked = false; + std::uint32_t descriptorOffset = 0; + if (address && immediateU32(offset, descriptorOffset)) { + return TryMakeIndirectColumn(handle, *heapHandle, sampler, words, descriptorOffset, first, plan); + } if (!MatchAffineOffset(offset, key, stride, addend)) { if (!MatchMaskedOffset(offset, key, stride)) { return false; @@ -637,6 +641,68 @@ class Tracker { return true; } + bool TryMakeIndirectColumn(IrValue& handle, IrValue& pointer, bool sampler, std::uint32_t words, std::uint32_t descriptorOffset, std::uint32_t descriptorImmediate, TableColumnPlan& plan) { + if (pointer.Opcode() != IrOpcode::GetAddressResource || pointer.ArgumentCount() != 2u) { + return false; + } + IrValue* bufferHandle = nullptr; + IrValue* offset = nullptr; + std::uint32_t first = 0; + for (std::uint32_t dword = 0; dword < 2u; dword++) { + IrValue* read = pointer.Argument(dword)->Resolve(); + std::uint32_t memoryIndex = 0; + const MemoryInfo* memory = ScalarReadMemory(*read, memoryIndex); + if (memory == nullptr || !MemoryIndexBelongsTo(memoryIndex, *read)) { + return false; + } + if (dword == 0u) { + first = memory->offset; + bufferHandle = read->Argument(0)->Resolve(); + offset = read->Argument(1)->Resolve(); + } else if (memory->offset != first + static_cast<std::uint32_t>(sizeof(std::uint32_t)) || read->Argument(0)->Resolve() != bufferHandle || !EquivalentValue(m_program.Resources(), offset, read->Argument(1))) { + return false; + } + } + IrValue* key = nullptr; + std::uint32_t stride = 0; + std::uint32_t addend = 0; + if (!MatchAffineOffset(offset, key, stride, addend)) { + return false; + } + DescriptorSource heapSource; + std::uint32_t heapIndex = 0; + if (!MakeRuntimeSource(*bufferHandle, IrOpcode::GetBufferResource, 4u, heapIndex, heapSource)) { + return false; + } + TableColumn column; + column.heapSource = heapIndex; + column.stride = stride; + column.addend = addend; + column.offset = first; + column.dwordCount = words; + column.maxKey = possibleU32Bits(key); + column.sampler = sampler; + column.indirect = true; + column.indirectOffset = descriptorOffset; + column.indirectImmediate = descriptorImmediate; + const auto domain = MatchKeyDomain(key); + DescriptorSource columnSource = heapSource; + columnSource.tableColumn = column; + plan.handle = &handle; + plan.source = InternSource(columnSource); + plan.keyDomain = domain; + auto& shared = *m_sources[plan.source].tableColumn; + shared.maxKey = std::max(shared.maxKey, column.maxKey); + if (!domain.has_value()) { + shared.unkeyed = true; + shared.keyDomains.clear(); + } else if (!shared.unkeyed && std::find(shared.keyDomains.begin(), shared.keyDomains.end(), *domain) == shared.keyDomains.end()) { + shared.keyDomains.push_back(*domain); + } + plan.offset = offset; + return true; + } + const TableColumnPlan* FindTableColumn(const IrValue& handle) const { const auto found = std::ranges::find_if(m_tableColumns, [&](const TableColumnPlan& plan) { return plan.handle == &handle; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index ede17b76e5e..a0f5e958bed 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -107,8 +107,8 @@ static_assert(sizeof(IrResourcePlan) == 584, "IrResourcePlan changed: update Enc static_assert(sizeof(IrValue) == 152, "IrValue changed: update the plan value encoder"); static_assert(sizeof(GuestRegister) == 8, "GuestRegister changed: update the plan value encoder"); static_assert(sizeof(MemoryInfo) == 80, "MemoryInfo changed: update the plan encoder"); -static_assert(sizeof(DescriptorSource) == 144, "DescriptorSource changed: update the plan encoder"); -static_assert(sizeof(TableColumn) == 64, "TableColumn changed: update the plan encoder"); +static_assert(sizeof(DescriptorSource) == 152, "DescriptorSource changed: update the plan encoder"); +static_assert(sizeof(TableColumn) == 72, "TableColumn changed: update the plan encoder"); static_assert(sizeof(KeyDomain) == 16, "KeyDomain changed: update the plan encoder"); static_assert(sizeof(ResourceBlock) == 80, "ResourceBlock changed: update the plan encoder"); static_assert(sizeof(SrtRead) == 16, "SrtRead changed: update the plan encoder"); @@ -148,7 +148,7 @@ static_assert(fieldCount<IrResourcePlan>() == 21, "IrResourcePlan changed: updat static_assert(fieldCount<GuestRegister>() == 2, "GuestRegister changed: update the plan value encoder"); static_assert(fieldCount<MemoryInfo>() == 30, "MemoryInfo changed: update the plan encoder"); static_assert(fieldCount<DescriptorSource>() == 3, "DescriptorSource changed: update the plan encoder"); -static_assert(fieldCount<TableColumn>() == 10, "TableColumn changed: update the plan encoder"); +static_assert(fieldCount<TableColumn>() == 13, "TableColumn changed: update the plan encoder"); static_assert(fieldCount<KeyDomain>() == 4, "KeyDomain changed: update the plan encoder"); static_assert(fieldCount<ResourceBlock>() == 4, "ResourceBlock changed: update the plan encoder"); static_assert(fieldCount<SrtRead>() == 2, "SrtRead changed: update the plan encoder"); @@ -1019,6 +1019,9 @@ bool encodePlan(Writer& writer, const IrResourcePlan& plan) { domains.Value(domain.value); }); out.Value(column.unkeyed); + out.Value(column.indirect); + out.Value(column.indirectOffset); + out.Value(column.indirectImmediate); }); writer.List(plan.controlFlow, [&](Writer& out, const ResourceBlock& block) { out.Value(valueIndex(block.condition)); @@ -1169,6 +1172,9 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) { domains.Value(domain.value); }); in.Value(column.unkeyed); + in.Value(column.indirect); + in.Value(column.indirectOffset); + in.Value(column.indirectImmediate); source.tableColumn = table ? std::optional(std::move(column)) : std::nullopt; }); reader.List(plan.controlFlow, 28, [&](Reader& in, ResourceBlock& block) { From 55eefe9c5eb9a67a20183147aa68308c033192ca Mon Sep 17 00:00:00 2001 From: Alex Collini <pipodi93@gmail.com> Date: Wed, 14 Oct 2026 17:02:06 +0200 Subject: [PATCH 55/81] feat(agc): plan table columns whose descriptor spans two loads of a record A descriptor whose dwords come from two scalar buffer loads of the same record, where one load's offset carries an immediate addend, was refused because the column wanted one offset expression and one buffer node for every dword. The column now peels an immediate addend off each read's offset and compares effective offsets (memory offset plus addend) and the remaining base expressions, and accepts distinct buffer nodes that are equivalent. Dword 0 keeps its offset expression, so columns that were already planned are planned the same way. Folding a second load into the first changes where its offset wraps: at a dynamic offset of 0xfffffff0, a second load at +16 wraps to the start of the buffer, while the folded read lands past 4 GiB. A column folds a load only when its addend is no smaller than dword 0's and its immediate is small, and it records dword 0's addend and the largest extra addend. A record whose offset would carry past 32 bits between the two loads is not read and becomes OffsetCarry poison. PPSA26344 pixel shader 0x8000296000 reads T# dwords 0-3 with an s_buffer_load_dwordx16 at key * 0x88 and dwords 4-7 with an s_buffer_load_dwordx4 at 48 + key * 0x88. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 53 +++++++++++++++++++ .../tests/execution/ImageTable.cpp | 31 +++++++++++ core/shader/recompiler/ImageTableAbi.hpp | 2 + .../IrMetadata/ControlFlowInfo.hpp | 4 +- .../Optimization/src/ResourceMaterializer.cpp | 10 +++- .../Optimization/src/ResourceTracker.cpp | 35 ++++++++++-- core/shader/recompiler/ShaderDiskCache.cpp | 10 ++-- 7 files changed, 136 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 59cf1d71b5c..8d8cba88c6e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1648,6 +1648,57 @@ void verifyPointerImageTable() { #endif } +void verifyPointerTableOffsetCarry() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + constexpr std::size_t Low = 0x10000; + constexpr std::uint64_t High = 0xffff0000ull; + constexpr std::size_t HighBytes = 0x20000; + constexpr std::uint64_t Reserved = High + HighBytes; + static TableTexture textures[2]; + void* block = nullptr; +#ifdef _WIN32 + block = VirtualAlloc(nullptr, static_cast<SIZE_T>(Reserved), MEM_RESERVE, PAGE_NOACCESS); + require(block != nullptr && VirtualAlloc(block, Low, MEM_COMMIT, PAGE_READWRITE) == block && VirtualAlloc(static_cast<std::byte*>(block) + High, HighBytes, MEM_COMMIT, PAGE_READWRITE) == static_cast<std::byte*>(block) + High, "pointer table offset carry: cannot map the root"); +#else + block = mmap(nullptr, Reserved, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0); + require(block != MAP_FAILED && mprotect(block, Low, PROT_READ | PROT_WRITE) == 0 && mprotect(static_cast<std::byte*>(block) + High, HighBytes, PROT_READ | PROT_WRITE) == 0, "pointer table offset carry: cannot map the root"); +#endif + auto* root = static_cast<std::uint32_t*>(block); + const auto base = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(root)); + static std::array<std::uint32_t, 8> keys{}; + static std::array<std::uint32_t, 4> output{}; + const auto keysV = TableV(keys.data(), 0u, static_cast<std::uint32_t>(sizeof(keys))); + const auto outputV = TableV(output.data(), 0u, static_cast<std::uint32_t>(sizeof(output))); + std::copy(keysV.begin(), keysV.end(), root); + std::copy(outputV.begin(), outputV.end(), root + 4); + const auto at = [&](std::uint64_t offset, std::array<std::uint32_t, 8> words) { + std::copy(words.begin(), words.end(), root + offset / 4u); + }; + at(0x100000010ull, TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D)); + at(0x50u, TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D)); + const std::array<std::uint32_t, 2> userData{static_cast<std::uint32_t>(base), static_cast<std::uint32_t>(base >> 32u)}; + const std::vector<std::uint32_t> code{0xf4080200u, 0xfa000000u, 0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0x7e200500u, 0x8f108210u, 0xf4200444u, + 0x20000000u, 0x8711ff11u, 0x000000ffu, 0x8f128511u, 0x8113ff12u, 0xffffff10u, 0x8114ff12u, 0xffffff20u, 0xf4080900u, 0x26000040u, 0xf4080a00u, + 0x28000040u, 0xf09c0f08u, 0x01090000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + auto request = TableRequest(code, userData); + request.shader.codeAddress = 0x36000u; + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "pointer table offset carry: a T# read by two loads of its record was not planned as a table"); + const auto& column = *plan->descriptorSources.at(plan->info.images[0].source).tableColumn; + require(column.address && column.stride == 32u && column.addend == 0xffffff10u && column.offset == 0x40u && column.maxKey == 0xffu && column.foldedAddend == 0xffffff10u && column.foldedCarry == 16u, "pointer table offset carry: the column is wrong"); + const auto compiled = CompileTable(request); + const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0); + require(snapshot.keys == 256u && snapshot.codes[7] == Abi::PoisonCode(static_cast<std::uint32_t>(Abi::PoisonReason::OffsetCarry)), "pointer table offset carry: the record whose second load wraps past 32 bits is not OffsetCarry poison"); + require((snapshot.codes[6] & Abi::PoisonFlag) == 0u && (snapshot.codes[8] & Abi::PoisonFlag) == 0u && compiled.capture->snapshot.tables.words.at(snapshot.codes[6]).dwords[0] == root[0x100000010ull / 4u] && compiled.capture->snapshot.tables.words.at(snapshot.codes[8]).dwords[0] == root[0x50u / 4u] && root[0x100000010ull / 4u] != root[0x50u / 4u], "pointer table offset carry: the records on either side of the carry were not resolved"); + require(!TableTouched(compiled.regions, root + 0x100000030ull / 4u, 0x20u), "pointer table offset carry: the record whose second load wraps was read"); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + munmap(block, Reserved); +#endif +} + void verifyDescriptorPhis() { using namespace ShaderRecompiler; constexpr std::uint32_t Format8888UNorm = 56; @@ -4802,6 +4853,7 @@ int main(int argc, char** argv) { verifyImageTableRegistration(); verifyScalarAddressRule(); verifyPointerImageTable(); + verifyPointerTableOffsetCarry(); std::cout << "Image table keys, maps, records and registration passed\n"; return 0; } @@ -4825,6 +4877,7 @@ int main(int argc, char** argv) { verifyImageTableRegistration(); verifyScalarAddressRule(); verifyPointerImageTable(); + verifyPointerTableOffsetCarry(); verifyDescriptorPhis(); verifyUniformTableImage(); verifyLaneSelectedDescriptors(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index 4fd08658280..43f1799617f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -237,6 +237,13 @@ alignas(256) constexpr std::array<std::uint32_t, 25> ReadPointerCode{ 0xbf810000u, }; +alignas(256) constexpr std::array<std::uint32_t, 30> SplitRecordCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0x8f108202u, 0xf4200444u, + 0x20000000u, 0x9312b011u, 0xf42c0802u, 0x24000000u, 0xf4280a02u, 0x24000020u, 0x360200bfu, 0x7e020d01u, + 0x060202f0u, 0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0xf09c0f08u, 0x01090401u, 0x7e120202u, + 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + alignas(256) constexpr std::array<std::uint32_t, 32> IndirectRecordCode{ 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, 0xf4240b02u, 0x24000008u, 0xf40c0916u, 0xfa000020u, @@ -1099,6 +1106,29 @@ void RunPointerRecordTests(AgcDriver::VulkanDevice& device) { RequireRed(changed, 1, Lanes, texels[0], "float texels after the T# behind record 1's pointer changed"); } +void RunSplitRecordTests(AgcDriver::VulkanDevice& device) { + Fill(); + const auto floatImage = Image(FloatTexels.data(), Format32Float); + const auto uintImage = Image(UintTexels.data(), Format32UInt); + const auto sintImage = Image(SintTexels.data(), Format32SInt); + const std::array<std::array<std::uint32_t, 4>, 3> images{floatImage, uintImage, sintImage}; + const std::array<std::function<std::uint32_t(std::uint32_t)>, 3> texels{ + [](std::uint32_t x) { return std::bit_cast<std::uint32_t>(FloatTexels[x]); }, + [](std::uint32_t x) { return UintTexels[x]; }, + [](std::uint32_t x) { return static_cast<std::uint32_t>(SintTexels[x]); }, + }; + const std::array<const char*, 3> names{"float", "uint", "sint"}; + + Table = {}; + for (std::uint32_t record = 0; record < 3; ++record) SetRecord(record, PointClamp, images[record]); + Keys = {}; + for (std::uint32_t group = 0; group < 3; ++group) Keys[group] = 2u - group; + const auto split = Run(device, SplitRecordCode, 3, Lanes, 64); + Require(!Faulted(split), "image table: a T# read by two loads of its record faulted:\n" + split.log); + Require(split.imageTableCount == 3u, "image table: a T# read by two loads of its record does not hold one element per distinct T# (" + std::to_string(split.imageTableCount) + ")"); + for (std::uint32_t group = 0; group < 3; ++group) RequireRed(split, group, Lanes, texels[2u - group], std::string(names[2u - group]) + " texels of a T# read by two loads of its record"); +} + void RunPointerStorageWriteTests(AgcDriver::VulkanDevice& device) { alignas(256) static std::array<std::uint32_t, 256u * 8u> records{}; alignas(256) static std::array<std::uint32_t, 256u * 8u> otherRecords{}; @@ -1299,6 +1329,7 @@ int main() { RunStoreTests(*device); RunPointerTests(*device); RunPointerRecordTests(*device); + RunSplitRecordTests(*device); RunPointerStorageWriteTests(*device); RunGraphicsTests(*device); std::puts("image table tests passed"); diff --git a/core/shader/recompiler/ImageTableAbi.hpp b/core/shader/recompiler/ImageTableAbi.hpp index 8a07ef7cdae..9b434141d8a 100644 --- a/core/shader/recompiler/ImageTableAbi.hpp +++ b/core/shader/recompiler/ImageTableAbi.hpp @@ -75,6 +75,7 @@ enum class PoisonReason : std::uint32_t { HeapWritten, DriverRejected, UnsupportedOperation, + OffsetCarry, Count }; @@ -101,6 +102,7 @@ constexpr std::string_view PoisonReasonName(PoisonReason reason) { case PoisonReason::HeapWritten: return "table written by its dispatch"; case PoisonReason::DriverRejected: return "rejected by the driver"; case PoisonReason::UnsupportedOperation: return "unsupported operation"; + case PoisonReason::OffsetCarry: return "record offset carries past 32 bits"; case PoisonReason::Count: break; } return "unknown"; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp index dd2c624c312..c402bbee7ea 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp @@ -42,9 +42,11 @@ struct TableColumn { bool indirect = false; std::uint32_t indirectOffset = 0; std::uint32_t indirectImmediate = 0; + std::uint32_t foldedAddend = 0; + std::uint32_t foldedCarry = 0; bool operator==(const TableColumn& other) const { - return heapSource == other.heapSource && stride == other.stride && addend == other.addend && offset == other.offset && dwordCount == other.dwordCount && sampler == other.sampler && address == other.address && indirect == other.indirect && indirectOffset == other.indirectOffset && indirectImmediate == other.indirectImmediate; + return heapSource == other.heapSource && stride == other.stride && addend == other.addend && offset == other.offset && dwordCount == other.dwordCount && sampler == other.sampler && address == other.address && indirect == other.indirect && indirectOffset == other.indirectOffset && indirectImmediate == other.indirectImmediate && foldedAddend == other.foldedAddend && foldedCarry == other.foldedCarry; } }; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 4ecd7e90f2f..7353276efbb 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -345,7 +345,7 @@ void reportTables() { static_cast<unsigned long long>(counters.narrowed.exchange(0, std::memory_order_relaxed)), static_cast<unsigned long long>(counters.narrowSkipped.exchange(0, std::memory_order_relaxed)), static_cast<double>(counters.nanoseconds.exchange(0, std::memory_order_relaxed)) / 1e6); } -enum class RecordState : std::uint8_t { OutsideDomain, Read, Unmapped }; +enum class RecordState : std::uint8_t { OutsideDomain, Read, Unmapped, Carried }; struct ColumnRecords { std::uint64_t base = 0; @@ -643,6 +643,10 @@ class TableSnapshotter { for (std::uint32_t record = 0; record < records.keys; record++) { if (records.states[record] != RecordState::Read) continue; const auto offset = column.addend + record * column.stride; + if (offset >= column.foldedAddend && offset > 0xffffffffu - column.foldedCarry) { + records.states[record] = RecordState::Carried; + continue; + } const auto head = locate(offset, column.offset); const auto tail = locate(offset, column.offset + last); if (head.has_value() && tail.has_value() && (*head & 3u) == 0u && *tail > *head && *tail - *head == last && wholeAccessible(*head) && wholeAccessible(*tail)) { @@ -730,6 +734,10 @@ class TableSnapshotter { out.codes[key] = ImageTableAbi::PoisonCode(static_cast<std::uint32_t>(PoisonReason::Unmapped)); counters.poison[static_cast<std::size_t>(PoisonReason::Unmapped)].fetch_add(1, std::memory_order_relaxed); break; + case RecordState::Carried: + out.codes[key] = ImageTableAbi::PoisonCode(static_cast<std::uint32_t>(PoisonReason::OffsetCarry)); + counters.poison[static_cast<std::size_t>(PoisonReason::OffsetCarry)].fetch_add(1, std::memory_order_relaxed); + break; } trace.poison++; } diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 11e737fa973..6bce45abcad 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -539,6 +539,17 @@ class Tracker { return KeyDomain{sourceIndex, memory->offset, std::max(4u, 1u << zeros)}; } + static IrValue* SplitImmediateAddend(IrValue* value, std::uint32_t& addend) { + value = value->Resolve(); + addend = 0; + if (value->Opcode() == IrOpcode::IAdd32 && value->ArgumentCount() == 2u) { + if (immediateU32(value->Argument(0), addend)) return value->Argument(1)->Resolve(); + if (immediateU32(value->Argument(1), addend)) return value->Argument(0)->Resolve(); + } + addend = 0; + return value; + } + bool TryMakeTableColumn(IrValue& handle, bool sampler, bool r128, TableColumnPlan& plan) { const auto expected = sampler ? IrOpcode::GetSamplerResource : IrOpcode::GetImageResource; const std::uint32_t width = sampler ? 4u : 8u; @@ -558,6 +569,10 @@ class Tracker { } IrValue* heapHandle = nullptr; IrValue* offset = nullptr; + IrValue* base = nullptr; + std::uint32_t baseAddend = 0; + std::uint32_t carry = 0; + bool folded = false; std::uint32_t first = 0; bool address = false; for (std::uint32_t dword = 0; dword < words; dword++) { @@ -568,20 +583,30 @@ class Tracker { if (memory == nullptr || !MemoryIndexBelongsTo(memoryIndex, *read)) { return false; } + std::uint32_t readAddend = 0; + IrValue* readBase = SplitImmediateAddend(read->Argument(1), readAddend); if (dword == 0u) { first = memory->offset; address = addressRead; - } else if (addressRead != address || memory->offset != first + dword * static_cast<std::uint32_t>(sizeof(std::uint32_t))) { + base = readBase; + baseAddend = readAddend; + } else if (addressRead != address || memory->offset + readAddend != first + baseAddend + dword * static_cast<std::uint32_t>(sizeof(std::uint32_t))) { return false; + } else if (readAddend != baseAddend) { + if (readAddend < baseAddend || static_cast<std::uint64_t>(memory->offset) + (readAddend - baseAddend) > 0x7fffffffu) { + return false; + } + folded = true; + carry = std::max(carry, readAddend - baseAddend); } IrValue* currentHandle = read->Argument(0)->Resolve(); - if (heapHandle != nullptr && currentHandle != heapHandle) { + if (heapHandle != nullptr && currentHandle != heapHandle && !EquivalentValue(m_program.Resources(), heapHandle, currentHandle)) { return false; } - heapHandle = currentHandle; + if (heapHandle == nullptr) heapHandle = currentHandle; if (dword == 0u) { offset = read->Argument(1)->Resolve(); - } else if (!EquivalentValue(m_program.Resources(), offset, read->Argument(1))) { + } else if (!EquivalentValue(m_program.Resources(), base, readBase)) { return false; } plan.memory.push_back(memoryIndex); @@ -623,6 +648,8 @@ class Tracker { column.maxKey = possibleU32Bits(key); column.sampler = sampler; column.address = address; + column.foldedAddend = folded ? baseAddend : 0u; + column.foldedCarry = folded ? carry : 0u; const auto domain = masked ? std::optional<KeyDomain>{} : MatchKeyDomain(key); DescriptorSource columnSource = heapSource; columnSource.tableColumn = column; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index a0f5e958bed..1858e4721dd 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -107,8 +107,8 @@ static_assert(sizeof(IrResourcePlan) == 584, "IrResourcePlan changed: update Enc static_assert(sizeof(IrValue) == 152, "IrValue changed: update the plan value encoder"); static_assert(sizeof(GuestRegister) == 8, "GuestRegister changed: update the plan value encoder"); static_assert(sizeof(MemoryInfo) == 80, "MemoryInfo changed: update the plan encoder"); -static_assert(sizeof(DescriptorSource) == 152, "DescriptorSource changed: update the plan encoder"); -static_assert(sizeof(TableColumn) == 72, "TableColumn changed: update the plan encoder"); +static_assert(sizeof(DescriptorSource) == 160, "DescriptorSource changed: update the plan encoder"); +static_assert(sizeof(TableColumn) == 80, "TableColumn changed: update the plan encoder"); static_assert(sizeof(KeyDomain) == 16, "KeyDomain changed: update the plan encoder"); static_assert(sizeof(ResourceBlock) == 80, "ResourceBlock changed: update the plan encoder"); static_assert(sizeof(SrtRead) == 16, "SrtRead changed: update the plan encoder"); @@ -148,7 +148,7 @@ static_assert(fieldCount<IrResourcePlan>() == 21, "IrResourcePlan changed: updat static_assert(fieldCount<GuestRegister>() == 2, "GuestRegister changed: update the plan value encoder"); static_assert(fieldCount<MemoryInfo>() == 30, "MemoryInfo changed: update the plan encoder"); static_assert(fieldCount<DescriptorSource>() == 3, "DescriptorSource changed: update the plan encoder"); -static_assert(fieldCount<TableColumn>() == 13, "TableColumn changed: update the plan encoder"); +static_assert(fieldCount<TableColumn>() == 15, "TableColumn changed: update the plan encoder"); static_assert(fieldCount<KeyDomain>() == 4, "KeyDomain changed: update the plan encoder"); static_assert(fieldCount<ResourceBlock>() == 4, "ResourceBlock changed: update the plan encoder"); static_assert(fieldCount<SrtRead>() == 2, "SrtRead changed: update the plan encoder"); @@ -1022,6 +1022,8 @@ bool encodePlan(Writer& writer, const IrResourcePlan& plan) { out.Value(column.indirect); out.Value(column.indirectOffset); out.Value(column.indirectImmediate); + out.Value(column.foldedAddend); + out.Value(column.foldedCarry); }); writer.List(plan.controlFlow, [&](Writer& out, const ResourceBlock& block) { out.Value(valueIndex(block.condition)); @@ -1175,6 +1177,8 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) { in.Value(column.indirect); in.Value(column.indirectOffset); in.Value(column.indirectImmediate); + in.Value(column.foldedAddend); + in.Value(column.foldedCarry); source.tableColumn = table ? std::optional(std::move(column)) : std::nullopt; }); reader.List(plan.controlFlow, 28, [&](Reader& in, ResourceBlock& block) { From b8ed78902f66a17cb4e1a095d0fc05c2cdbe6c6a Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:19:24 +0200 Subject: [PATCH 56/81] fix(input): read the input file from paths with non-ASCII characters Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../prx/libSceVideoOut/src/InputMapping.cpp | 33 +++++++++++--- .../prx/libSceVideoOut/tests/InputMapping.cpp | 43 +++++++++++++++++++ docs/user/INPUT_MAPPING.md | 2 +- 3 files changed, 70 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp index d974db9e754..334984d1f71 100644 --- a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp @@ -214,13 +214,32 @@ std::uint8_t stickByte(std::int32_t axis) { return static_cast<std::uint8_t>(((axis + 32768) * 255 + 32767) / 65535); } +std::filesystem::path pathFromUtf8(std::string_view text) { + return std::filesystem::path(std::u8string(reinterpret_cast<const char8_t*>(text.data()), text.size())); +} + +std::string pathToUtf8(const std::filesystem::path& path) { + const auto text = path.u8string(); + return std::string(reinterpret_cast<const char*>(text.data()), text.size()); +} + +std::filesystem::path configuredPath() { +#ifdef _WIN32 + const wchar_t* configured = _wgetenv(L"ANYPS5_INPUT_CONFIG"); + return configured != nullptr ? std::filesystem::path(configured) : std::filesystem::path{}; +#else + const char* configured = std::getenv("ANYPS5_INPUT_CONFIG"); + return configured != nullptr ? pathFromUtf8(configured) : std::filesystem::path{}; +#endif +} + [[noreturn]] void invalidLine(const std::filesystem::path& path, std::size_t line, const std::string& reason) { - throw std::runtime_error("Pad: invalid input mapping " + path.string() + ":" + std::to_string(line) + ": " + reason); + throw std::runtime_error("Pad: invalid input mapping " + pathToUtf8(path) + ":" + std::to_string(line) + ": " + reason); } std::filesystem::path defaultConfigPath() { std::unique_ptr<char, decltype(&SDL_free)> basePath(SDL_GetBasePath(), SDL_free); - if (basePath) return std::filesystem::path(basePath.get()) / "anyps5-input.ini"; + if (basePath) return pathFromUtf8(basePath.get()) / "anyps5-input.ini"; return "anyps5-input.ini"; } @@ -244,13 +263,13 @@ std::array<std::uint8_t, 2> Pad::StickWithDeadzone(std::int16_t x, std::int16_t Pad::InputConfiguration Pad::LoadInputMapping() { InputConfiguration configuration{{InputMapping.begin(), InputMapping.end()}, {}}; auto& bindings = configuration.bindings; - const char* configuredPath = std::getenv("ANYPS5_INPUT_CONFIG"); - const bool explicitPath = configuredPath != nullptr && configuredPath[0] != '\0'; - const std::filesystem::path path = explicitPath ? configuredPath : defaultConfigPath(); + const auto configured = configuredPath(); + const bool explicitPath = !configured.empty(); + const std::filesystem::path path = explicitPath ? configured : defaultConfigPath(); std::ifstream file(path); if (!file) { if (explicitPath || std::filesystem::exists(path)) { - throw std::runtime_error("Pad: cannot read input mapping '" + path.string() + "'"); + throw std::runtime_error("Pad: cannot read input mapping '" + pathToUtf8(path) + "'"); } return configuration; } @@ -301,7 +320,7 @@ Pad::InputConfiguration Pad::LoadInputMapping() { invalidLine(path, lineNumber, error.what()); } } - if (file.bad()) throw std::runtime_error("Pad: cannot read input mapping '" + path.string() + "'"); + if (file.bad()) throw std::runtime_error("Pad: cannot read input mapping '" + pathToUtf8(path) + "'"); return configuration; } diff --git a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp index daa2dad4d6f..1fe0f4b30b0 100644 --- a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp @@ -105,6 +105,48 @@ void mouseLookReject() { require(rejects("MouseDecayMs = 20\nmousedecayms = 30\n"), "a mouse setting given twice is rejected"); } +void setConfigPath(const std::filesystem::path& path) { +#ifdef _WIN32 + require(_wputenv_s(L"ANYPS5_INPUT_CONFIG", path.wstring().c_str()) == 0, "set ANYPS5_INPUT_CONFIG"); +#else + require(::setenv("ANYPS5_INPUT_CONFIG", path.string().c_str(), 1) == 0, "set ANYPS5_INPUT_CONFIG"); +#endif +} + +void nonAsciiPaths() { + const std::u8string name = u8"anyps5-input-\u00fc-\u65e5\u672c-" + std::u8string(root.filename().u8string()); + const auto directory = root / std::filesystem::path(name); + std::filesystem::create_directories(directory); + const auto file = directory / "anyps5-input.ini"; + { + std::ofstream stream(file, std::ios::binary | std::ios::trunc); + stream << "MouseSensitivity = 3\n"; + require(stream.good(), "write the input mapping in a non-ASCII directory"); + } + setConfigPath(file); + require(Pad::LoadInputMapping().settings.mouseSensitivity == 3.0, "ANYPS5_INPUT_CONFIG may name a path with non-ASCII characters"); + { + std::ofstream stream(file, std::ios::binary | std::ios::trunc); + stream << "MouseSensitivity = fast\n"; + } + std::string message; + try { + Pad::LoadInputMapping(); + } catch (const std::runtime_error& error) { + message = error.what(); + } + const auto utf8Name = std::string(reinterpret_cast<const char*>(name.data()), name.size()); + require(message.find(utf8Name) != std::string::npos, "an error names the path in UTF-8"); + setConfigPath(directory / "missing.ini"); + message.clear(); + try { + Pad::LoadInputMapping(); + } catch (const std::runtime_error& error) { + message = error.what(); + } + require(message.find(utf8Name) != std::string::npos, "a missing file names the path in UTF-8"); +} + void deadzoneOffMatchesRawSticks() { for (const std::int16_t axis : {std::int16_t{-32768}, std::int16_t{-20000}, std::int16_t{-1}, std::int16_t{0}, std::int16_t{1}, std::int16_t{1234}, std::int16_t{32767}}) { const auto stick = Pad::StickWithDeadzone(axis, axis, 0); @@ -141,6 +183,7 @@ int main() try { mouseLookDefault(); mouseLookParse(); mouseLookReject(); + nonAsciiPaths(); deadzoneOffMatchesRawSticks(); deadzoneShapesSticks(); std::filesystem::remove_all(root); diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index d61e36d70a6..54f26dc6eda 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -1,6 +1,6 @@ # Controller, keyboard and mouse input -AnyPS5 accepts SDL-mapped game controllers as well as its built-in keyboard and mouse bindings. Xbox-style controllers and PlayStation controllers recognized by SDL use the standard PS button layout; sticks and analog triggers are passed through, and controllers can be connected or disconnected while the game is running. The first recognized controller is used. AnyPS5 uses its built-in keyboard and mouse bindings when no configuration file is present. To change selected keyboard and mouse bindings, create `anyps5-input.ini` beside the generated game executable. Set `ANYPS5_INPUT_CONFIG` to use a file at another path. +AnyPS5 accepts SDL-mapped game controllers as well as its built-in keyboard and mouse bindings. Xbox-style controllers and PlayStation controllers recognized by SDL use the standard PS button layout; sticks and analog triggers are passed through, and controllers can be connected or disconnected while the game is running. The first recognized controller is used. AnyPS5 uses its built-in keyboard and mouse bindings when no configuration file is present. To change selected keyboard and mouse bindings, create `anyps5-input.ini` beside the generated game executable. Set `ANYPS5_INPUT_CONFIG` to use a file at another path. Both the game directory and that path may contain any characters, such as `ü` or `日本`. For an Xbox-layout controller, A/B/X/Y map to Cross/Circle/Square/Triangle, LB/RB map to L1/R1, and LT/RT map to analog L2/R2. Start maps to Options. D-pad and stick-click buttons are supported. On a PlayStation controller, the touchpad click button is forwarded; finger touch coordinates are not implemented. From a53332472c92669794d34d4d881a2581f2c3e8bd Mon Sep 17 00:00:00 2001 From: Eli Yarson <eli.yarson@gmail.com> Date: Wed, 14 Oct 2026 10:25:22 -0300 Subject: [PATCH 57/81] feat(agc): report which trace threads dropped events and where their buffers filled The trace summary line keeps `trace_dropped` as before: true when any thread lost a slice, any thread's totals table was full, or the gpu track was full. When it is true, the line now also carries `trace_dropped_threads`, one entry per affected thread name, so the reader can tell which thread lost data and how much. Threads that share a name, such as the shader prepare threads, share one entry: losses are summed, the earliest fill is kept and totals_full is true if any is. Each entry has `events_full_after_s`, the seconds from the first present of the window to the start of the first lost slice. It is absent for `gpu` and in every summary line (windowed frames/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 last present. `events_lost` is the number of slices not recorded and is absent when 0; in interval lines it counts the buffer's losses since it started. `totals_full` is true when the thread's totals table overflowed, which makes the summary numbers incomplete; a thread with only `events_lost` has complete totals. The Chrome trace gets a `trace buffer full` instant event on a thread at the point where its buffer filled. BUILD.md documents the new fields and the event. `trace_dropped` is unchanged, and when nothing drops the default output is byte-identical to before. Refs #5786 Depends on #5808 --- .../Execution/include/PerformanceTrace.hpp | 54 +++++++++++-- .../Execution/src/PerformanceTrace.cpp | 3 +- .../tests/PerformanceTrace.cpp | 79 +++++++++++++++++++ docs/dev/BUILD.md | 4 +- 4 files changed, 131 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp index 435a8ef4908..2a5314f9ffa 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp @@ -162,6 +162,9 @@ class TraceThread { std::span<const Slice> slices; std::vector<Total> totals; bool dropped = false; + std::uint64_t eventsLost = 0; + std::optional<Clock::time_point> eventsFullAt; + bool totalsFull = false; }; inline static const std::atomic<bool> AlwaysBuffering{true}; @@ -195,7 +198,10 @@ class TraceThread { if (scope == nullptr) continue; snapshot.totals.push_back({scope, entry.stage.load(std::memory_order_relaxed), Clock::duration(entry.length.load(std::memory_order_relaxed)), entry.count.load(std::memory_order_relaxed)}); } - snapshot.dropped = dropped.load(std::memory_order_relaxed); + snapshot.eventsLost = lost.load(std::memory_order_acquire); + if (snapshot.eventsLost != 0) snapshot.eventsFullAt = Clock::time_point(Clock::duration(firstLost.load(std::memory_order_relaxed))); + snapshot.totalsFull = totalsFull.load(std::memory_order_relaxed); + snapshot.dropped = snapshot.eventsLost != 0 || snapshot.totalsFull; return snapshot; } @@ -210,7 +216,9 @@ class TraceThread { void append(const Slice& slice) { const auto index = used.load(std::memory_order_relaxed); if (index == capacity) { - dropped.store(true, std::memory_order_relaxed); + const auto count = lost.load(std::memory_order_relaxed); + if (count == 0) firstLost.store(slice.begin.time_since_epoch().count(), std::memory_order_relaxed); + lost.store(count + 1, std::memory_order_release); return; } slices[index] = slice; @@ -233,7 +241,7 @@ class TraceThread { entry.count.store(entry.count.load(std::memory_order_relaxed) + 1, std::memory_order_relaxed); return; } - dropped.store(true, std::memory_order_relaxed); + totalsFull.store(true, std::memory_order_relaxed); } const std::string name; @@ -242,7 +250,9 @@ class TraceThread { const std::unique_ptr<Slice[]> slices; const std::size_t capacity; std::atomic<std::size_t> used{0}; - std::atomic<bool> dropped{false}; + std::atomic<std::uint64_t> lost{0}; + std::atomic<Clock::rep> firstLost{0}; + std::atomic<bool> totalsFull{false}; std::array<Entry, 512> entries; bool sampled = false; std::uint64_t sampledPresent = 0; @@ -344,6 +354,10 @@ class TraceFormat { } output += text; } + if (thread.eventsFullAt) { + event("trace buffer full", "i", *thread.eventsFullAt, tid); + output += ",\"s\":\"t\",\"args\":{\"events_lost\":" + std::to_string(thread.eventsLost) + "}}"; + } } for (std::size_t index = 0; index + 1 < presents.size(); ++index) { event("present", "i", presents[index], presenter + 1); @@ -423,8 +437,22 @@ class TraceFormat { bool firstWorker = true; std::map<std::string, Clock::duration> phases; bool dropped = false; + struct DroppedThread { + std::string name; + std::uint64_t eventsLost = 0; + std::optional<Clock::time_point> eventsFullAt; + bool totalsFull = false; + }; + std::vector<DroppedThread> droppedList; for (const auto& thread : threads) { dropped = dropped || thread.dropped; + if (thread.eventsLost != 0 || thread.totalsFull) { + auto entry = std::find_if(droppedList.begin(), droppedList.end(), [&](const DroppedThread& candidate) { return candidate.name == thread.name; }); + if (entry == droppedList.end()) entry = droppedList.insert(droppedList.end(), DroppedThread{thread.name}); + entry->eventsLost += thread.eventsLost; + if (thread.eventsLost != 0 && thread.eventsFullAt && (!entry->eventsFullAt || *thread.eventsFullAt < *entry->eventsFullAt)) entry->eventsFullAt = thread.eventsFullAt; + entry->totalsFull = entry->totalsFull || thread.totalsFull; + } Clock::duration busy{}; for (const auto& total : thread.totals) { const std::string_view scope = total.scope; @@ -447,6 +475,20 @@ class TraceFormat { } output += "},\"trace_dropped\":"; output += dropped ? "true" : "false"; + if (!droppedList.empty()) { + std::string droppedThreads; + for (const auto& entry : droppedList) { + if (!droppedThreads.empty()) droppedThreads += ','; + AppendString(droppedThreads, entry.name); + droppedThreads += ":{"; + if (entry.eventsLost != 0) { + if (entry.eventsFullAt && !presents.empty() && *entry.eventsFullAt >= presents.front() && *entry.eventsFullAt <= presents.back()) droppedThreads += "\"events_full_after_s\":" + number(std::chrono::duration<double>(*entry.eventsFullAt - presents.front()).count()) + ","; + droppedThreads += "\"events_lost\":" + std::to_string(entry.eventsLost) + ","; + } + droppedThreads += std::string("\"totals_full\":") + (entry.totalsFull ? "true" : "false") + "}"; + } + output += ",\"trace_dropped_threads\":{" + droppedThreads + "}"; + } output += ",\"trace\":"; AppendString(output, summary.trace); output += "}\n"; @@ -483,7 +525,7 @@ class PerformanceTrace { if (!buffering.load(std::memory_order_relaxed)) return; std::lock_guard lock(gpuMutex); if (gpuRanges.size() == gpuRanges.capacity()) { - gpuDropped = true; + ++gpuLost; return; } gpuRanges.push_back({name, beginNs, endNs, batch}); @@ -509,7 +551,7 @@ class PerformanceTrace { inline static thread_local TraceThread* local = nullptr; inline static std::mutex gpuMutex; inline static std::vector<TraceGpuRange> gpuRanges; - inline static bool gpuDropped = false; + inline static std::uint64_t gpuLost = 0; }; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp index 862c10d8486..c913b43155e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp @@ -221,6 +221,7 @@ void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const { std::lock_guard gpu(gpuMutex); gpuRanges.clear(); + gpuLost = 0; gpuRanges.reserve(GpuRangeCapacity); } state.presents.push_back(now); @@ -256,7 +257,7 @@ void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const if (state.gpuClock && device != nullptr && device->CalibrateClock(clock.gpuNs, clock.cpu)) { std::lock_guard gpu(gpuMutex); gpuSlices = TraceFormat::GpuSlices(gpuRanges, *state.gpuClock, clock, state.presents.front()); - snapshots.push_back({"gpu", gpuSlices, {}, gpuDropped}); + snapshots.push_back({"gpu", gpuSlices, {}, gpuLost != 0, gpuLost}); } const auto process = processId(); auto summary = identity(width, height, device); diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp index 3f16b22de47..41824053d68 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp @@ -57,9 +57,31 @@ void BufferCap() { for (int index = 0; index < 3; ++index) thread.Add("Driver.Packet", nullptr, origin + milliseconds(index), origin + milliseconds(index) + microseconds(30)); const auto snapshot = thread.Collect(); Check(snapshot.slices.size() == 2 && snapshot.dropped, "a full buffer did not stop and set the dropped flag"); + Check(snapshot.eventsLost == 1 && snapshot.eventsFullAt == origin + milliseconds(2) && !snapshot.totalsFull, "a full buffer did not report the lost slice and its time apart from the totals"); Check(snapshot.totals.size() == 1 && snapshot.totals[0].count == 3 && snapshot.totals[0].length == microseconds(90), "totals missed a slice past the cap"); } +void LostEventsCount() { + std::atomic<std::uint64_t> presents{1}; + TraceThread thread("queue worker 0x0", presents, 2); + const auto origin = Clock::now(); + Check(!thread.Collect().eventsFullAt && thread.Collect().eventsLost == 0, "a thread that lost nothing reported a loss"); + for (int index = 0; index < 10; ++index) thread.Add("Driver.Packet", nullptr, origin + milliseconds(index), origin + milliseconds(index) + microseconds(30)); + const auto snapshot = thread.Collect(); + Check(snapshot.eventsLost == 8 && snapshot.eventsFullAt == origin + milliseconds(2), "the lost slices were not counted from the first one"); +} + +void TotalsOverflow() { + static char scopes[600]; + std::atomic<std::uint64_t> presents{1}; + TraceThread thread("presenter", presents, 4096); + const auto origin = Clock::now(); + for (const auto& scope : scopes) thread.Add(&scope, nullptr, origin, origin + microseconds(30)); + const auto snapshot = thread.Collect(); + Check(snapshot.totalsFull && snapshot.dropped && snapshot.eventsLost == 0 && !snapshot.eventsFullAt, "a full totals table was not reported apart from the event buffer"); + Check(snapshot.totals.size() == 512 && snapshot.slices.size() == 600, "a full totals table stopped the event buffer or kept more than its entries"); +} + void CapacitySettings() { Check(TraceThread::ParseCapacity(nullptr) == TraceThread::DefaultCapacity, "the default event capacity changed"); Check(TraceThread::ParseCapacity("1") == 1, "the smallest event capacity was rejected"); @@ -152,6 +174,22 @@ void BatchIds() { Check(TraceFormat::GpuSlices(ranges, first, last, origin)[0].batch == 3, "a GPU range lost its batch id"); } +void FullBufferEvents() { + const auto origin = Clock::now(); + const std::vector<TraceThread::Slice> slices{{"Driver.Packet", nullptr, origin + milliseconds(1), microseconds(30)}}; + TraceThread::Snapshot snapshot{"queue worker 0x0", slices, {}, true, 7, origin + milliseconds(3), false}; + const std::vector<TraceThread::Snapshot> threads{snapshot}; + const std::vector<Clock::time_point> presents{origin, origin + milliseconds(16)}; + const auto json = TraceFormat::Events(7, origin, threads, 0, presents, 5); + const std::string expected = + "{\"name\":\"trace buffer full\",\"ph\":\"i\",\"ts\":3000.000,\"pid\":7,\"tid\":1,\"s\":\"t\",\"args\":{\"events_lost\":7}},\n" + "{\"name\":\"present\""; + Check(json.find(expected) != std::string::npos, "the cut-off instant event is missing or has the wrong shape"); + snapshot.eventsLost = 0; + snapshot.eventsFullAt.reset(); + Check(TraceFormat::Events(7, origin, std::vector<TraceThread::Snapshot>{snapshot}, 0, presents, 5).find("trace buffer full") == std::string::npos, "a thread that lost nothing got a cut-off event"); +} + void SummaryFields() { const auto origin = Clock::now(); AgcDriver::TraceSummary summary; @@ -184,6 +222,24 @@ void SummaryFields() { auto dropped = threads; dropped[1].dropped = true; Check(TraceFormat::Summary(summary, dropped, presents).find("\"trace_dropped\":true") != std::string::npos, "a dropped thread did not mark the summary"); + Check(line.find("trace_dropped_threads") == std::string::npos, "a summary without drops listed threads"); + dropped[0].dropped = true; + dropped[0].eventsLost = 2240000; + dropped[0].eventsFullAt = presents.front() + milliseconds(32); + dropped[1].totalsFull = true; + const auto detailed = TraceFormat::Summary(summary, dropped, presents); + Check(detailed.find("\"trace_dropped\":true,\"trace_dropped_threads\":{\"queue worker 0x0\":{\"events_full_after_s\":0.032,\"events_lost\":2240000,\"totals_full\":false},\"presenter\":{\"totals_full\":true}},\"trace\":") != std::string::npos, "the dropped threads were not listed with their reasons"); + auto shared = threads; + shared.push_back({"shader prepare", {}, {}, true}); + shared.push_back({"shader prepare", {}, {}, true}); + shared[2].eventsLost = 5; + shared[2].eventsFullAt = presents.front() + milliseconds(30); + shared[3].eventsLost = 7; + shared[3].eventsFullAt = presents.front() + milliseconds(10); + shared[3].totalsFull = true; + const auto merged = TraceFormat::Summary(summary, shared, presents); + Check(merged.find("\"trace_dropped_threads\":{\"shader prepare\":{\"events_full_after_s\":0.010,\"events_lost\":12,\"totals_full\":true}},\"trace\":") != std::string::npos, "threads with one name were not merged into one entry"); + Check(merged.find("shader prepare") == merged.rfind("shader prepare"), "threads with one name were listed twice"); } void GpuClock() { @@ -271,6 +327,25 @@ void IntervalSummaryFields() { Check(line == expected, "interval summary line changed"); } +void IntervalDroppedThreads() { + const auto origin = Clock::now(); + AgcDriver::TraceSummary summary; + summary.interval = std::make_pair(30.0, 60.0); + std::vector<TraceThread::Snapshot> threads{{"queue worker 0x0", {}, {}, true, 5}}; + const std::vector<Clock::time_point> presents{origin + milliseconds(100), origin + milliseconds(116)}; + threads[0].eventsFullAt = origin + milliseconds(40); + auto line = TraceFormat::Summary(summary, threads, presents); + Check(line.find("\"queue worker 0x0\":{\"events_lost\":5,\"totals_full\":false}") != std::string::npos, "a loss before the interval printed its time"); + threads[0].eventsFullAt = origin + milliseconds(104); + line = TraceFormat::Summary(summary, threads, presents); + Check(line.find("{\"events_full_after_s\":0.004,\"events_lost\":5,") != std::string::npos, "a loss inside the interval lost its time"); + threads[0].eventsFullAt = origin + milliseconds(120); + line = TraceFormat::Summary(summary, threads, presents); + Check(line.find("\"queue worker 0x0\":{\"events_lost\":5,\"totals_full\":false}") != std::string::npos && line.find("events_full_after_s") == std::string::npos, "a loss after the last present printed its time"); + line = TraceFormat::Summary(summary, threads, {}); + Check(line.find("\"queue worker 0x0\":{\"events_lost\":5,\"totals_full\":false}") != std::string::npos && line.find("events_full_after_s") == std::string::npos, "an interval without presents printed a loss time"); +} + void GzipFile() { std::string text; for (int index = 0; index < 20000; ++index) text += "{\"name\":\"Driver.Packet\",\"ph\":\"X\",\"ts\":" + std::to_string(index) + "}\n"; @@ -323,7 +398,10 @@ int main() { WindowBySeconds(); WindowRejects(); BufferCap(); + LostEventsCount(); + TotalsOverflow(); CapacitySettings(); + FullBufferEvents(); FoldShortSlices(); EscapeStrings(); EventsShape(); @@ -334,6 +412,7 @@ int main() { SummaryOnlyThread(); TotalsSince(); IntervalSummaryFields(); + IntervalDroppedThreads(); GzipFile(); AppendLines(); std::cout << "performance trace tests passed\n"; diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index a3bd5422831..0b1680c82a5 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -151,9 +151,9 @@ Set `APS5_TRACE_FRAMES=<start>:<count>` to record `<count>` presents from presen Set `APS5_TRACE_EVENTS=<count>` 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 32 bytes per event): 8 MiB at the default, 96 MiB at `3145728`, and 128 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, and 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 `<thread> 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 and sets `trace_dropped`. When the window closes, the presenter formats the trace once and a `ProfileOutput` writer thread writes `aps5-trace-<commit>-<pid>.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 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 `<thread> 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-<commit>-<pid>.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 `<path>`), `gpu`, `driver`, `os`, `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` and `trace`. A trace holds timings, scope names, counts and thread names; no guest memory, paths or strings of the title. +The summary line has `commit` and `dirty` of the build, `env` (every `APS5_*` variable; a value containing `/` or `\` is written as `<path>`), `gpu`, `driver`, `os`, `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. The shader preparation threads are traced as well, `shader prepare`, with a buffer for 32,768 events each. A wait for the GPU mutex on a traced thread is a `GpuMutex.wait.<site>` slice (`dispatch`, `draw`, `present`, `flush` and so on, as on the `[lock]` line). From 52a3b157968384ed1e0a538702e7e4dc4af913d3 Mon Sep 17 00:00:00 2001 From: Itamar Medyoni <itamar.medyoni@gmail.com> Date: Wed, 14 Oct 2026 15:09:11 +0300 Subject: [PATCH 58/81] feat(libkernel): report relative symbolic links in lstat on Windows lstat of a Windows symbolic link threw, although symlink creates links with relative targets on Windows and readlink reads them. Report such a link as S_IFLNK with its own identity and times and the length of its target as st_size, through the reparse reader readlink already used, now shared from libkernel. Links with absolute targets, junctions and other reparse points still throw, as for readlink. --- .../File/include/WindowsSymbolicLink.hpp | 58 +++++++++++++++++++ .../prx/libkernel/File/src/NativeStat.cpp | 22 +++++-- core/libs/prx/libkernel_sys/Export.cpp | 41 +------------ core/libs/tests/GuestFilesystem.cpp | 29 +++++++--- docs/dev/debt/libkernel.md | 1 + 5 files changed, 101 insertions(+), 50 deletions(-) create mode 100644 core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp diff --git a/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp new file mode 100644 index 00000000000..849b88004e6 --- /dev/null +++ b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp @@ -0,0 +1,58 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_WINDOWSSYMBOLICLINK_HPP +#define CORE_LIBS_PRX_LIBKERNEL_FILE_WINDOWSSYMBOLICLINK_HPP + +#include <windows.h> +#include <winioctl.h> +#include <array> +#include <cstddef> +#include <cstring> +#include <filesystem> +#include <stdexcept> +#include <string> +#include <system_error> + +namespace File { + +class WindowsSymbolicLink { + struct ReparseData { + ULONG tag; + USHORT dataLength; + USHORT reserved; + USHORT substituteNameOffset; + USHORT substituteNameLength; + USHORT printNameOffset; + USHORT printNameLength; + ULONG flags; + }; + static constexpr ULONG relativeFlag = 1; + +public: + static bool ReadRelative(const std::filesystem::path& native, std::string& target, const char* operation) { + const HANDLE handle = CreateFileW(native.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) + throw std::system_error(static_cast<int>(GetLastError()), std::system_category(), std::string(operation) + ": CreateFileW"); + std::array<std::byte, MAXIMUM_REPARSE_DATA_BUFFER_SIZE> data{}; + DWORD size = 0; + const BOOL read = DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, nullptr, 0, data.data(), static_cast<DWORD>(data.size()), &size, nullptr); + const DWORD code = GetLastError(); + CloseHandle(handle); + if (!read) + throw std::system_error(static_cast<int>(code), std::system_category(), std::string(operation) + ": FSCTL_GET_REPARSE_POINT"); + ReparseData header{}; + if (size < sizeof(header)) return false; + std::memcpy(&header, data.data(), sizeof(header)); + if (header.tag != IO_REPARSE_TAG_SYMLINK || (header.flags & relativeFlag) == 0) return false; + const std::size_t begin = sizeof(header) + header.substituteNameOffset; + if (begin + header.substituteNameLength > size) + throw std::runtime_error(std::string(operation) + ": malformed symbolic link reparse data"); + std::wstring name(header.substituteNameLength / sizeof(wchar_t), L'\0'); + std::memcpy(name.data(), data.data() + begin, name.size() * sizeof(wchar_t)); + target = std::filesystem::path(name).generic_string(); + return true; + } +}; + +} + +#endif diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index 5e6e304a73b..7950599933b 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -8,14 +8,17 @@ #ifdef _WIN32 #include "prx/libkernel/File/include/WindowsFileTime.hpp" +#include "prx/libkernel/File/include/WindowsSymbolicLink.hpp" #include <io.h> #include <sys/stat.h> #include <sys/types.h> +constexpr unsigned short SymbolicLinkMode = 0120777; struct NativeStat : __stat64 { FILE_BASIC_INFO times{}; BY_HANDLE_FILE_INFORMATION identity{}; bool hasTimes = false; bool hasIdentity = false; + bool isSymbolicLink = false; }; static int ReadHandleInfo(HANDLE handle, NativeStat* st) { if (GetFileType(handle) != FILE_TYPE_DISK) return 0; @@ -45,9 +48,20 @@ static int DoFstat(int fd, NativeStat* st) { } static int DoLstat(const std::filesystem::path& p, NativeStat* st) { const auto attributes = GetFileAttributesW(p.c_str()); - if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) - NotImplemented_nid_no_patch("lstat of a Windows symbolic link"); - return DoStat(p, st); + if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return DoStat(p, st); + std::string target; + if (!File::WindowsSymbolicLink::ReadRelative(p, target, "lstat")) + NotImplemented_nid_no_patch("lstat of a Windows reparse point other than a relative symbolic link"); + const auto handle = CreateFileW(p.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) return File::WindowsFileTime::Failure(GetLastError()); + const int result = ReadHandleInfo(handle, st); + CloseHandle(handle); + if (result != 0) return result; + st->st_mode = SymbolicLinkMode; + st->st_size = static_cast<__int64>(target.size()); + st->isSymbolicLink = true; + return 0; } #else #include <sys/stat.h> @@ -68,7 +82,7 @@ static void CopyNativeStat(const NativeStat& st, FileStat* sb) { sb->st_mode = static_cast<std::uint16_t>(st.st_mode); sb->st_size = static_cast<std::int64_t>(st.st_size); #ifdef _WIN32 - if (st.hasIdentity) sb->st_size = static_cast<std::int64_t>((static_cast<std::uint64_t>(st.identity.nFileSizeHigh) << 32u) | st.identity.nFileSizeLow); + if (st.hasIdentity && !st.isSymbolicLink) sb->st_size = static_cast<std::int64_t>((static_cast<std::uint64_t>(st.identity.nFileSizeHigh) << 32u) | st.identity.nFileSizeLow); sb->st_dev = static_cast<std::uint32_t>(st.st_dev); sb->st_ino = static_cast<std::uint32_t>(st.st_ino); sb->st_nlink = static_cast<std::uint16_t>(st.st_nlink); diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 67c48233462..99809d62129 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -1,5 +1,4 @@ #include <algorithm> -#include <array> #include <cstdarg> #include <cstdint> #include <cstddef> @@ -30,6 +29,7 @@ #include <windows.h> #include <io.h> #include <sys/stat.h> +#include "prx/libkernel/File/include/WindowsSymbolicLink.hpp" #ifndef SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE #define SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE 0x2 #endif @@ -125,49 +125,12 @@ std::int64_t CopyLinkTarget(const std::string& target, char* buffer, std::size_t return static_cast<std::int64_t>(count); } -#ifdef _WIN32 -struct SymbolicLinkReparseData { - ULONG tag; - USHORT dataLength; - USHORT reserved; - USHORT substituteNameOffset; - USHORT substituteNameLength; - USHORT printNameOffset; - USHORT printNameLength; - ULONG flags; -}; -constexpr ULONG SymbolicLinkRelative = 1; - -bool ReadRelativeSymbolicLink(const std::filesystem::path& native, std::string& target) { - const HANDLE handle = CreateFileW(native.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, - OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); - if (handle == INVALID_HANDLE_VALUE) - throw std::system_error(static_cast<int>(GetLastError()), std::system_category(), "readlink: CreateFileW"); - std::array<std::byte, MAXIMUM_REPARSE_DATA_BUFFER_SIZE> data{}; - DWORD size = 0; - const BOOL read = DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, nullptr, 0, data.data(), static_cast<DWORD>(data.size()), &size, nullptr); - const DWORD code = GetLastError(); - CloseHandle(handle); - if (!read) throw std::system_error(static_cast<int>(code), std::system_category(), "readlink: FSCTL_GET_REPARSE_POINT"); - SymbolicLinkReparseData header{}; - if (size < sizeof(header)) return false; - std::memcpy(&header, data.data(), sizeof(header)); - if (header.tag != IO_REPARSE_TAG_SYMLINK || (header.flags & SymbolicLinkRelative) == 0) return false; - const std::size_t begin = sizeof(header) + header.substituteNameOffset; - if (begin + header.substituteNameLength > size) throw std::runtime_error("readlink: malformed symbolic link reparse data"); - std::wstring name(header.substituteNameLength / sizeof(wchar_t), L'\0'); - std::memcpy(name.data(), data.data() + begin, name.size() * sizeof(wchar_t)); - target = std::filesystem::path(name).generic_string(); - return true; -} -#endif - std::int64_t ReadNativeLink(const std::filesystem::path& native, char* buffer, std::size_t size) { #ifdef _WIN32 const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { std::string target; - if (!ReadRelativeSymbolicLink(native, target)) { + if (!File::WindowsSymbolicLink::ReadRelative(native, target, "readlink")) { NotImplemented_nid_no_patch("readlink of a Windows reparse point other than a relative symbolic link"); return -1; } diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 854cf0fe330..9af2df93edf 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -651,9 +651,26 @@ int main() { Require(std::filesystem::create_directory(targetDirectory)); const auto directoryLink = root / "directory-link"; const auto danglingLink = root / "dangling-link"; + const auto absoluteLink = root / "absolute-link"; Require(CreateSymbolicLinkW(directoryLink.c_str(), L"target-directory", SYMBOLIC_LINK_FLAG_DIRECTORY | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); - Require(CreateSymbolicLinkW(danglingLink.c_str(), missing.c_str(), SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); + Require(CreateSymbolicLinkW(danglingLink.c_str(), L"missing.txt", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); + Require(CreateSymbolicLinkW(absoluteLink.c_str(), std::filesystem::absolute(present).c_str(), + SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); + const auto checkRelativeLink = [](const std::filesystem::path& link, std::int64_t targetLength) { + FileStat own{}; + return lstat_nid_postfix(link.string().c_str(), &own) == 0 && (own.st_mode & 0170000) == 0120000 && + own.st_size == targetLength && own.st_nlink == 1; + }; + Require(checkRelativeLink(fileLink, 11)); + Require(checkRelativeLink(directoryLink, 16)); + Require(checkRelativeLink(danglingLink, 11)); + FileStat followed{}; + Require(lstat_nid_postfix(fileLink.string().c_str(), &linkStatus) == 0); + Require(stat_nid_postfix(fileLink.string().c_str(), &followed) == 0 && (followed.st_mode & 0170000) == 0100000); + Require(followed.st_ino == status.st_ino && linkStatus.st_ino != status.st_ino && linkStatus.st_dev == status.st_dev); + Require(stat_nid_postfix(directoryLink.string().c_str(), &followed) == 0 && (followed.st_mode & 0170000) == 0040000); + Require(stat_nid_postfix(danglingLink.string().c_str(), &followed) == -1 && *__error_nid_postfix() == 2); const auto checkUnsupportedLink = [](const std::filesystem::path& link) { FileStat untouched; std::memset(&untouched, 0x5a, sizeof(untouched)); @@ -663,7 +680,7 @@ int main() { try { lstat_nid_postfix(link.string().c_str(), &untouched); } catch (const std::runtime_error& error) { - rejected = std::strcmp(error.what(), "lstat of a Windows symbolic link not implemented") == 0; + rejected = std::strcmp(error.what(), "lstat of a Windows reparse point other than a relative symbolic link not implemented") == 0; } const bool errorUnchanged = *__error_nid_postfix() == sentinelError; const auto* bytes = reinterpret_cast<const unsigned char*>(&untouched); @@ -671,11 +688,9 @@ int main() { return rejected && errorUnchanged && statusUnchanged; }; Require(stat_nid_postfix(fileLink.string().c_str(), &status) == 0 && status.st_size == 5); - Require(checkUnsupportedLink(fileLink)); - Require(checkUnsupportedLink(directoryLink)); - Require(checkUnsupportedLink(danglingLink)); - Require(std::filesystem::remove(fileLink) && std::filesystem::remove(directoryLink) && - std::filesystem::remove(danglingLink) && std::filesystem::remove(targetDirectory)); + Require(checkUnsupportedLink(absoluteLink)); + Require(std::filesystem::remove(fileLink) && std::filesystem::remove(directoryLink) && DeleteFileW(danglingLink.c_str()) != 0 && + std::filesystem::remove(absoluteLink) && std::filesystem::remove(targetDirectory)); } #endif FileStat identity{}; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6eaefa8c882..67831f40707 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -110,6 +110,7 @@ - [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes`, `futimes`, and `lutimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Guest directory descriptors still resolve through their recorded path. Filesystems with coarser timestamp resolution cannot retain every guest microsecond - [EVFILT_VNODE on macOS](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) registers `EVFILT_VNODE` on a duplicate of the descriptor in a host kqueue, whose notes have FreeBSD's values, and drops the `NOTE_LINK` that XNU adds to `NOTE_DELETE` when the watched file or directory is removed, since FreeBSD's `vop_remove_post` and `vop_rmdir_post` report `NOTE_DELETE` alone. As on Windows, a directory registered with `NOTE_EXTEND` is accepted but never receives it: XNU reports a move between directories as `NOTE_WRITE` alone, with `NOTE_LINK` for a subdirectory - [Windows file identity](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) - `stat` and `fstat` report the volume serial number as `st_dev` and the 64-bit file index folded into the guest's 32-bit `st_ino` (never 0), so two different files can in principle share an `st_ino`; the fold is not taken from a reference. `st_nlink` is clamped to 65535, the width of the guest field. The call keeps the `_wstat64` values when `GetFileInformationByHandle` fails +- [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A link with an absolute target, a junction and any other reparse point throw, as for `readlink` - [setitimer](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `sys_setitimer` and `kern_setitimer`: `EINVAL` for timers other than `ITIMER_REAL`, `ITIMER_VIRTUAL` and `ITIMER_PROF`, and for a value, or the interval of a non-zero value, with a negative field, `tv_usec` of 1000000 or more, or more than `INT32_MAX / 2` seconds; a null value reads the timer as `getitimer` does, with `EFAULT` when the output is null too. `ITIMER_REAL` shares the timer thread of `alarm`, which clears its interval: a zero value disarms it, the previous value is returned, and each expiry re-arms it by `it_interval`. An expiry delivers nothing: with `SIG_IGN` it is dropped, with the default disposition the process exits with status 142, and with a handler, or with `SIGALRM` blocked, it throws, since guest `SIGALRM` handlers are never run and signals are never left pending. The value is not raised to `tick / 16` as `itimerfix` does, and an interval below 1 ms throws, since the console's `hz`, and so that rounding, is unknown (1 ms covers any `hz` of 63 or more). `ITIMER_VIRTUAL` and `ITIMER_PROF` throw when armed, as process CPU time is not accounted, and read and disarm as zero. PPSA99420 imports it and fails to load without it - On Linux, [direct memory](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again. - [system](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified From cd0c6ed16ec89c2ee4f56df4661a491105f894b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Wed, 14 Oct 2026 18:55:36 +0200 Subject: [PATCH 59/81] test(libkernel): do not stat the dangling link on Windows stat of a dangling link reaches FillFileStat and throws on the CI toolchain; it is not part of this change. Co-Authored-By: Claude <noreply@anthropic.com> --- core/libs/tests/GuestFilesystem.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 9af2df93edf..2c17a9dc9c9 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -670,7 +670,6 @@ int main() { Require(stat_nid_postfix(fileLink.string().c_str(), &followed) == 0 && (followed.st_mode & 0170000) == 0100000); Require(followed.st_ino == status.st_ino && linkStatus.st_ino != status.st_ino && linkStatus.st_dev == status.st_dev); Require(stat_nid_postfix(directoryLink.string().c_str(), &followed) == 0 && (followed.st_mode & 0170000) == 0040000); - Require(stat_nid_postfix(danglingLink.string().c_str(), &followed) == -1 && *__error_nid_postfix() == 2); const auto checkUnsupportedLink = [](const std::filesystem::path& link) { FileStat untouched; std::memset(&untouched, 0x5a, sizeof(untouched)); From 30e85a3a2e95d28daaedd4d41909a3aa24c0d737 Mon Sep 17 00:00:00 2001 From: Alex Collini <pipodi93@gmail.com> Date: Mon, 12 Oct 2026 12:39:47 +0200 Subject: [PATCH 60/81] fix(shader): number an instanced draw's hull patches across its instances With GE_CNTL.BREAK_WAVE_AT_EOI clear, the instances of a tessellated draw share hull threadgroups: the relative patch index counts on through the instances, while the host's PrimitiveID restarts with each one. The local stage now passes its draw-relative instance (InstanceIndex - BaseInstance) to the hull, which numbers its patches instance * patches-per-instance + PrimitiveID before taking (mod PRIM_GRP_SIZE) mod NUM_PATCHES. Instances that start a threadgroup anyway (BREAK_WAVE_AT_EOI, or patches per instance a multiple of the group) keep the per-instance PrimitiveID. This replaces #4201's refusal of a hull or domain that uses its relative index as data while BREAK_WAVE_AT_EOI is clear, and builds on its decode of the bit (breakAtInstance, request serialization and draw keys). - Hull and domain cache keys hold the patches per instance instead of the bit; an indirect draw recompiles its hull and domain for each record's instance count. - The device enables shaderDrawParameters when available. Without it, a hull using its relative index as data in such a draw is refused, as is a domain doing so (passing the instance on to the domain is not implemented). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../Execution/src/Driver/Draw/Draw.cpp | 12 ++++-- .../Execution/src/ShaderDeviceProfile.cpp | 2 + .../Execution/src/VulkanDevice.cpp | 12 ++++++ .../Graphics/src/ShaderValidation.cpp | 3 +- .../libSceAgcDriver/tests/ShaderMemory.cpp | 27 +++++++++--- .../tests/execution/Tessellation.cpp | 37 ++++++++++++++-- core/shader/recompiler/CacheKey.hpp | 2 +- .../IrMetadata/StageIO.hpp | 5 ++- .../Optimization/ShaderStageInputInfo.hpp | 2 + .../Optimization/src/ShaderInfoCollector.cpp | 4 ++ .../Optimization/src/TessellationLowering.cpp | 42 +++++++++++++++++-- core/shader/recompiler/Recompiler.cpp | 9 +++- core/shader/recompiler/Recompiler.hpp | 7 ++++ .../SpirvBackend/SpirvEmitterState.hpp | 2 +- .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 2 + .../src/SpirvMemory/SpirvModuleSetup.cpp | 3 ++ .../SpirvBackend/src/SpirvModuleEmitter.cpp | 24 ++++++++--- .../src/ShaderInputInfoBuilder.cpp | 2 + 18 files changed, 170 insertions(+), 27 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index 8858b58f2c1..1121e825174 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -173,7 +173,8 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe std::vector<bool> recompiled(programs.size(), false); bool drawHit = false; bool verifyHit = false; - lookupDraw(submission, localDevice, graphics, pixel, programs, roles, vertexInfos, useDrawEntries, registerKey, profile, drawKey, entry, matched, matchedRegions, drawHit, verifyHit, phaseTiming, phaseMs); + const bool stageEntries = useDrawEntries && !(graphics.stages.tessellation && !drawParameters.indirect && drawParameters.instanceCount > 1u); + lookupDraw(submission, localDevice, graphics, pixel, programs, roles, vertexInfos, stageEntries, registerKey, profile, drawKey, entry, matched, matchedRegions, drawHit, verifyHit, phaseTiming, phaseMs); if (registerKey) { @@ -231,7 +232,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe pushCursorBytes += static_cast<std::uint32_t>(result.pushConstants.size()); } - cacheDrawStages(useDrawEntries, drawHit, drawParameters, indirectCpu, programs, stageCaptures, vertexInfos, decodeReads, verifyHit, matched, fresh, drawKey, registerKey, decode, phaseTiming); + cacheDrawStages(stageEntries, drawHit, drawParameters, indirectCpu, programs, stageCaptures, vertexInfos, decodeReads, verifyHit, matched, fresh, drawKey, registerKey, decode, phaseTiming); timing.Mark("shader_compile_and_link"); for (const auto& reads : decodeReads) { @@ -325,6 +326,11 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe patch(baseVertexWord, indirect.recordBytes == 20 ? arguments.vertexOffset : arguments.firstVertexOrIndex); patch(startInstanceWord, arguments.firstInstance); patch(drawIndexWord, record); + if (graphics.stages.tessellation) { + for (std::size_t i = 0; i < programs.size(); ++i) { + if (programs[i].binary.stage == Stage::TessellationControl || programs[i].binary.stage == Stage::TessellationEvaluation) patched.insert(i); + } + } Pm4::DrawParameters direct{0, arguments.count, 0, arguments.instances, drawParameters.flags, drawParameters.indexed, 0, 0}; if (drawParameters.indexed) { @@ -343,7 +349,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe auto& result = results[resultIndex[programIndex]]; const auto pushBytes = result.pushConstants.size(); decodeVertexInfo(programIndex); - result = *materializeDrawStage(programIndex, pushOffsets[programIndex], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected); + result = *materializeDrawStage(programIndex, pushOffsets[programIndex], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, programs[programIndex].binary.stage == Stage::TessellationControl || programs[programIndex].binary.stage == Stage::TessellationEvaluation ? direct : drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected); if (!rejected.empty()) return DrawVerdict::Rejected; require(result.pushConstants.size() == pushBytes, "patched program changed its push constant layout"); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp index 063205cbee2..7dc7a4640ca 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp @@ -30,6 +30,7 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta const auto* barycentric = findFeatures<VkPhysicalDeviceFragmentShaderBarycentricFeaturesKHR>(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_BARYCENTRIC_FEATURES_KHR); const auto* mesh = findFeatures<VkPhysicalDeviceMeshShaderFeaturesEXT>(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_FEATURES_EXT); const auto* clock = findFeatures<VkPhysicalDeviceShaderClockFeaturesKHR>(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_CLOCK_FEATURES_KHR); + const auto* drawParameters = findFeatures<VkPhysicalDeviceShaderDrawParametersFeatures>(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETERS_FEATURES); const auto* maintenance8 = findFeatures<VkPhysicalDeviceMaintenance8FeaturesKHR>(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_8_FEATURES_KHR); Require(target.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version, "shader device profile has an incompatible BDA ABI"); Require(bda != nullptr && bda->bufferDeviceAddress == VK_TRUE && core.shaderInt64 == VK_TRUE, "shader runtime requires enabled bufferDeviceAddress and shaderInt64"); @@ -64,6 +65,7 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta const bool meshEnabled = mesh != nullptr && mesh->meshShader == VK_TRUE; checkFeature(spv::CapabilityFragmentBarycentricKHR, barycentricEnabled, "fragmentShaderBarycentric"); checkFeature(spv::CapabilityMeshShadingEXT, meshEnabled, "meshShader"); + checkFeature(spv::CapabilityDrawParameters, drawParameters != nullptr && drawParameters->shaderDrawParameters == VK_TRUE, "shaderDrawParameters"); checkFeature(spv::CapabilityShaderClockKHR, clock != nullptr && clock->shaderSubgroupClock == VK_TRUE && clock->shaderDeviceClock == VK_TRUE, "shader clock"); Require(!target.fragmentShaderBarycentricEnabled || barycentricEnabled, "shader device profile exposes disabled barycentrics"); Require(!target.mesh || meshEnabled, "shader device profile exposes disabled mesh shaders"); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index c8830d353d9..eefe712a194 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1411,6 +1411,14 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : enabled.tessellationShader = available.tessellationShader; state->tessellationShader = enabled.tessellationShader == VK_TRUE; if (state->tessellationShader) state->capabilities.push_back(3); + VkPhysicalDeviceShaderDrawParametersFeatures drawParametersFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETERS_FEATURES}; + { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &drawParametersFeatures}; + state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &features); + } + const bool drawParameters = drawParametersFeatures.shaderDrawParameters == VK_TRUE; + drawParametersFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETERS_FEATURES, nullptr, VK_TRUE}; + if (drawParameters) state->capabilities.push_back(spv::CapabilityDrawParameters); APS5_LOG_OUT("Vulkan features tessellationAvailable=%u mesh=%u depthClip=%u depthRangeUnrestricted=%u pipelineLibrary=%u", static_cast<unsigned>(state->tessellationShader), static_cast<unsigned>(state->meshShader), static_cast<unsigned>(state->depthClipControl), static_cast<unsigned>(state->depthRangeUnrestricted), static_cast<unsigned>(state->graphicsPipelineLibrary)); require(available.samplerAnisotropy && available.textureCompressionBC, "device lacks sampler anisotropy or BC texture compression support required for texture sampling"); enabled.samplerAnisotropy = VK_TRUE; @@ -1603,6 +1611,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : descriptorIndexingFeatures.pNext = byteFeatures.pNext; byteFeatures.pNext = &descriptorIndexingFeatures; } + if (drawParameters) { + drawParametersFeatures.pNext = byteFeatures.pNext; + byteFeatures.pNext = &drawParametersFeatures; + } // Timeline semaphores let a queue worker wait for recorded batches without holding the GPU mutex // (see Recorder::WaitSerial). The instance is 1.1, so the KHR extension is used even on 1.2+ // devices. Debug aid: APS5_NO_TIMELINE=1 leaves it off (drains wait under the mutex as before). diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index 5e16fd73f0a..73a321a2d40 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -140,7 +140,7 @@ struct Module { } const auto signature = Signature(type); if (vertex && storage == spv::StorageClassInput) { - Require((value == spv::BuiltInVertexIndex || value == spv::BuiltInInstanceIndex) && signature == "i32", "unsupported vertex built-in input"); + Require((value == spv::BuiltInVertexIndex || value == spv::BuiltInInstanceIndex || value == spv::BuiltInBaseInstance) && signature == "i32", "unsupported vertex built-in input"); } else if (vertex && storage == spv::StorageClassOutput) { if (value == spv::BuiltInLayer && layerOutput && (signature == "u32" || signature == "i32")) return; Require(value == spv::BuiltInPosition && signature == "f32x4" && !position, "unsupported or duplicate vertex built-in output"); @@ -263,6 +263,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi capability == spv::CapabilityImageGatherExtended || capability == spv::CapabilityImageQuery || ((vertex || evaluation || mesh) && shaderClipDistance && capability == spv::CapabilityClipDistance) || + (vertex && capability == spv::CapabilityDrawParameters) || (fragment && geometryShader && capability == spv::CapabilityGeometry) || (fragment && sampleRateShading && capability == spv::CapabilitySampleRateShading) || capability == spv::CapabilityImageMSArray || diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 59cf1d71b5c..dd73fbc272d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -2508,16 +2508,31 @@ void verifyMeshConfiguration() { RecompileCacheKey::Build(primitiveGrouped, key); require((key != patchKey) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on the primitive group size, which rebuilds their relative patch index"); } - auto shared = hull; - shared.graphics->tessellation->breakAtInstance = false; + auto instanced = hull; + instanced.graphics->tessellation->breakAtInstance = false; + instanced.graphics->draw.instanceCount = 2u; + instanced.graphics->draw.indexCount = 12u; + auto longer = instanced; + longer.graphics->draw.indexCount = 16u; + auto breaking = instanced; + breaking.graphics->tessellation->breakAtInstance = true; for (const auto stage : {ShaderStage::Local, ShaderStage::TessellationControl, ShaderStage::TessellationEvaluation}) { hull.shader.stage = stage; - shared.shader.stage = stage; + instanced.shader.stage = stage; + longer.shader.stage = stage; + breaking.shader.stage = stage; RecompileCacheKey::Build(hull, key); - const auto patchKey = key; - RecompileCacheKey::Build(shared, key); - require((key != patchKey) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on BREAK_WAVE_AT_EOI, which decides whether their relative patch index can be rebuilt"); + const auto single = key; + RecompileCacheKey::Build(instanced, key); + const auto shared = key; + RecompileCacheKey::Build(longer, key); + require((shared != single && key != shared) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on the patches per instance of an instanced draw"); + RecompileCacheKey::Build(breaking, key); + require(key == single, "an instanced draw whose instances start hull threadgroups (BREAK_WAVE_AT_EOI) changed the cache key"); } + breaking.shader.stage = ShaderStage::TessellationControl; + const auto breakingReplay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(breaking)); + require(breakingReplay.request.graphics && breakingReplay.request.graphics->tessellation && breakingReplay.request.graphics->tessellation->breakAtInstance, "BREAK_WAVE_AT_EOI was lost in serialization"); const std::array<std::uint32_t, 8> users{}; request.context.waveSize = 32u; request.context.userData = users; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp index c016c7423a0..2946f45e8ba 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -23,6 +23,7 @@ #include <string> #include <tuple> #include <utility> +#include <vector> namespace { @@ -359,6 +360,8 @@ struct PatchDraw { std::span<const std::uint16_t> indices; float minLevel = 1.0f; float maxLevel = 64.0f; + std::uint32_t instances = 1; + std::uint32_t firstInstance = 0; }; alignas(256) constexpr std::array<std::uint16_t, 12> NextInputIndices{ @@ -394,7 +397,7 @@ ShaderRecompiler::RecompileResult Compile(const ShaderRecompiler::SpirvTarget& t pixel = info; } else { vertex = ShaderRecompiler::ShaderVertexStageInfo{}; - graphics = ShaderRecompiler::GraphicsCompileContext{hull ? 0u : 8u, {}, std::nullopt, patches.configuration, {reinterpret_cast<std::uintptr_t>(patches.indices.data()), static_cast<std::uint32_t>(patches.indices.size()), 2, 1}}; + graphics = ShaderRecompiler::GraphicsCompileContext{hull ? 0u : 8u, {}, std::nullopt, patches.configuration, {reinterpret_cast<std::uintptr_t>(patches.indices.data()), static_cast<std::uint32_t>(patches.indices.size()), 2, patches.instances}}; } auto layout = stage == ShaderStage::Fragment ? PixelPushLayout(pushCursor, target) : ShaderRecompiler::BindingLayout{0, 0, pushCursor, AgcDriver::Graphics::PipelinePushSlotBytes - pushCursor}; if (layout.pushConstantOffsetBytes < AgcDriver::Graphics::PipelinePushSlotBytes) layout.pushConstantSizeBytes = ShaderRecompiler::TessellationLevelPushOffsetBytes - layout.pushConstantOffsetBytes; @@ -477,7 +480,7 @@ void Draw(AgcDriver::VulkanDevice& device, const Stages& stages, VkCullModeFlags state.blend.colorWriteMask = 15; state.blends = {state.blend}; state.blendConstants = {}; - device.Draw(state, {reinterpret_cast<std::uintptr_t>(patches.indices.data()), static_cast<std::uint32_t>(patches.indices.size()), 2, 1, 0, true}, shaders); + device.Draw(state, {reinterpret_cast<std::uintptr_t>(patches.indices.data()), static_cast<std::uint32_t>(patches.indices.size()), 2, patches.instances, 0, true, 0, patches.firstInstance}, shaders); device.WaitIdle(); } @@ -621,6 +624,33 @@ void RequireRelativePatches(AgcDriver::VulkanDevice& device, const ShaderRecompi } } +void RequireInstancedRelativePatches(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { + const auto center = Row(-0.5f + 1.0f / 3.0f); + TessellationConfiguration restarting{3u, 3u, 1u, 2u, 2u, 4u, 8u}; + restarting.breakAtInstance = true; + for (const auto& [patches, first, last] : {std::tuple{PatchDraw{{3u, 3u, 1u, 2u, 2u, 4u, 8u}, Indices, 1.0f, 64.0f, 2u, 1u}, 128, 64}, std::tuple{PatchDraw{{3u, 3u, 1u, 2u, 2u, 3u, 3u}, Indices, 1.0f, 64.0f, 2u, 1u}, 32, 96}, std::tuple{PatchDraw{restarting, Indices, 1.0f, 64.0f, 2u, 1u}, 32, 96}}) { + std::uint32_t cursor = 0; + const Stages stages{ + Compile(target, ShaderStage::Local, LocalCode, cursor, patches), + Compile(target, ShaderStage::TessellationControl, HullRelativeData, cursor, patches), + Compile(target, ShaderStage::TessellationEvaluation, DomainPatchRead, cursor, patches), + Compile(target, ShaderStage::Fragment, PixelCode, cursor, patches), + }; + Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, patches); + for (const auto& [patch, expected] : {std::pair{0u, first}, std::pair{2u, last}}) { + const auto pixel = PixelAt(Column(patch, 0.25f), center); + Require(pixel[0] == 255 && std::abs(pixel[2] - expected) <= 1, "the patch in column " + std::to_string(patch) + " of an instanced draw with " + std::to_string(patches.configuration.patchesPerGroup) + " patches per group and " + std::to_string(patches.configuration.primitiveGroupSize) + " primitives per group" + (patches.configuration.breakAtInstance ? " and BREAK_WAVE_AT_EOI" : "") + " did not take its blue channel from the hull's relative patch index across instances, " + PixelText(pixel)); + } + } + const PatchDraw shared{{3u, 3u, 1u, 2u, 2u, 4u, 8u}, Indices, 1.0f, 64.0f, 2u, 1u}; + ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainRelativeData, "domain program uses its relative patch index as data in an instanced draw", shared); + std::vector<std::uint32_t> capabilities(target.supportedCapabilities.begin(), target.supportedCapabilities.end()); + std::erase(capabilities, static_cast<std::uint32_t>(spv::CapabilityDrawParameters)); + auto withoutDrawParameters = target; + withoutDrawParameters.supportedCapabilities = capabilities; + ExpectRefusal(withoutDrawParameters, ShaderStage::TessellationControl, HullRelativeData, "shaderDrawParameters", shared); +} + class GuestBlock { public: GuestBlock() { @@ -802,8 +832,6 @@ int main() { ExpectRefusal(target, ShaderStage::Local, LocalIndexAsData, "outside a tessellation ring or LDS address"); ExpectRefusal(target, ShaderStage::TessellationControl, HullRelativeData, "GE_CNTL.PRIM_GRP_SIZE is 0", RecordPatches); ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainRelativeData, "GE_CNTL.PRIM_GRP_SIZE is 0", RecordPatches); - ExpectRefusal(target, ShaderStage::TessellationControl, HullRelativeData, "GE_CNTL.BREAK_WAVE_AT_EOI is clear", {{3u, 3u, 1u, 2u, 2u, 3u, 3u}, Indices}); - ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainRelativeData, "GE_CNTL.BREAK_WAVE_AT_EOI is clear", {{3u, 3u, 1u, 2u, 2u, 3u, 3u}, Indices}); ExpectRefusal(target, ShaderStage::TessellationControl, HullCode, "outside its patch's quad factors", {{3u, 3u, 2u, 2u, 2u}, Indices}); ExpectRefusal(target, ShaderStage::TessellationControl, HullDescendingRecords, "not affine"); ExpectRefusal(target, ShaderStage::TessellationControl, HullDescendingRecords, "not affine", {{3u, 3u, 1u, 2u, 2u, 158u}, Indices}); @@ -822,6 +850,7 @@ int main() { RequireQuads(*device, target); RequirePatchConstants(*device, target); RequireRelativePatches(*device, target); + RequireInstancedRelativePatches(*device, target); RequireHullBdaReads(*device, target); RequireCompareBranches(target); device.reset(); diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 33e899a8188..e0264eeae7a 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -109,7 +109,7 @@ class RecompileCacheKey { if (request.shader.stage == ShaderStage::Local) return; append(key, tessellation->patchesPerGroup); append(key, tessellation->primitiveGroupSize); - append(key, tessellation->breakAtInstance); + append(key, TessellationInstancePatches(request)); } template<typename TValue> diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp index 96c7d515334..84ec2412d08 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp @@ -14,7 +14,9 @@ enum class TessellationAttribute { EvaluationInput, PatchOutput, Factor, - PatchInput + PatchInput, + InstanceOutput, + InstanceInput }; enum class TessellationBaseKind { @@ -46,6 +48,7 @@ enum class StageInputKind { HelperInvocation, RasterPrimitive, LayerExported, + BaseInstance, }; enum class StageOutputKind { diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 77e52d36de5..d23faa3437f 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -115,6 +115,8 @@ struct ShaderTessellationInputInfo { std::uint32_t patchesPerGroup = 256; std::uint32_t primitiveGroupSize = 0; bool breakAtInstance = false; + std::uint32_t instancePatches = 0; + bool drawParameters = false; }; struct ShaderVertexInputInfo { diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index 6ded41848d2..211f7b9a12b 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -151,6 +151,7 @@ void ValidateValueReferences(const IrProgram& program, ShaderStageInputInfo inpu break; case StageInputKind::VertexIndex: case StageInputKind::InstanceIndex: + case StageInputKind::BaseInstance: case StageInputKind::InvocationId: case StageInputKind::PrimitiveId: case StageInputKind::FrontFacing: @@ -343,6 +344,9 @@ void CollectBuiltinInputs(const IrProgram& program, ShaderInfo& info) { case StageInputKind::InstanceIndex: AddInput(info, kind, 0, 1, "gl_InstanceIndex"); break; + case StageInputKind::BaseInstance: + AddInput(info, kind, 0, 1, "gl_BaseInstance"); + break; case StageInputKind::InvocationId: AddInput(info, kind, 0, 1, "gl_InvocationID"); break; diff --git a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp index 5ca2dc0cc65..e1970d35c43 100644 --- a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp +++ b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp @@ -576,6 +576,7 @@ class Lowering { if (data) requireGroupedPatches(); for (IrValue* base : relative) base->ReplaceAllUsesWith(data ? &groupedPatch(*base) : &constant(0u)); deadCodeEliminator.Eliminate(program); + if (stage == IrShaderStage::Local && tessellation.drawParameters) writeInstance(); program.Metadata().tessellationLocalStride = localStride; program.Metadata().tessellationControlStride = controlStride; program.Metadata().tessellationPatchStride = patchStride; @@ -773,7 +774,9 @@ class Lowering { void requireGroupedPatches() const { if (stage == IrShaderStage::Local) Fail("the relative patch or vertex index is used outside a tessellation ring or LDS address", 0u); if (tessellation.primitiveGroupSize == 0u) Fail(stage == IrShaderStage::TessellationControl ? "the hull program uses its relative patch index as data, but GE_CNTL.PRIM_GRP_SIZE is 0 or not programmed" : "the domain program uses its relative patch index as data, but GE_CNTL.PRIM_GRP_SIZE is 0 or not programmed", 0u); - if (!tessellation.breakAtInstance) Fail(stage == IrShaderStage::TessellationControl ? "the hull program uses its relative patch index as data, but GE_CNTL.BREAK_WAVE_AT_EOI is clear, so the instances of a draw can share hull threadgroups" : "the domain program uses its relative patch index as data, but GE_CNTL.BREAK_WAVE_AT_EOI is clear, so the instances of a draw can share hull threadgroups", 0u); + if (tessellation.instancePatches == 0u || restartsPerInstance()) return; + if (stage == IrShaderStage::TessellationEvaluation) Fail("the domain program uses its relative patch index as data in an instanced draw whose instances share hull threadgroups (GE_CNTL.BREAK_WAVE_AT_EOI clear)", 0u); + if (!tessellation.drawParameters) Fail("the hull program uses its relative patch index as data in an instanced draw whose instances share hull threadgroups, which needs the device's shaderDrawParameters", 0u); } IrValue& groupedPatch(IrValue& base) { @@ -787,8 +790,14 @@ class Lowering { IrValue& count = constant(divisor); return emit(IrOpcode::ISub32, {&value, &emit(IrOpcode::IMul32, {&emit(IrOpcode::UDiv32, {&value, &count}), &count})}); }; - IrValue& primitive = emit(IrOpcode::GetBuiltin, {&constant(static_cast<std::uint32_t>(StageInputKind::PrimitiveId)), &constant(0u)}); - return remainder(remainder(primitive, tessellation.primitiveGroupSize), tessellation.patchesPerGroup); + IrValue* primitive = &emit(IrOpcode::GetBuiltin, {&constant(static_cast<std::uint32_t>(StageInputKind::PrimitiveId)), &constant(0u)}); + if (tessellation.instancePatches != 0u && !restartsPerInstance()) { + IrValue& always = program.CreateValue(IrOpcode::Void, IrType::Bool); + always.SetImmediateBool(true); + IrValue& instance = emit(IrOpcode::GetTessellationAttribute, {&constant(static_cast<std::uint32_t>(TessellationAttribute::InstanceInput)), &constant(0u), &always}); + primitive = &emit(IrOpcode::IAdd32, {&emit(IrOpcode::IMul32, {&instance, &constant(tessellation.instancePatches)}), primitive}); + } + return remainder(remainder(*primitive, tessellation.primitiveGroupSize), tessellation.patchesPerGroup); } void removeUnobservedCycles() { @@ -833,6 +842,33 @@ class Lowering { return result; } + [[nodiscard]] bool restartsPerInstance() const { + const auto patches = tessellation.instancePatches; + const auto group = tessellation.primitiveGroupSize; + return group != 0u && (patches % group == 0u || (group % tessellation.patchesPerGroup == 0u && patches % tessellation.patchesPerGroup == 0u)); + } + + void writeInstance() { + IrBlock* entry = program.BlockOrder().front(); + IrValue* first = entry->Instructions().empty() ? nullptr : entry->Instructions().front(); + const auto place = [&](IrOpcode opcode, IrType type, std::initializer_list<IrValue*> arguments) -> IrValue& { + IrValue& value = program.CreateValue(opcode, type); + for (IrValue* argument : arguments) value.AddArgument(argument); + if (first != nullptr) { + entry->InsertInstructionBefore(first, &value); + } else { + entry->AppendInstruction(&value); + } + return value; + }; + IrValue& index = place(IrOpcode::GetBuiltin, IrType::U32, {&constant(static_cast<std::uint32_t>(StageInputKind::InstanceIndex)), &constant(0u)}); + IrValue& base = place(IrOpcode::GetBuiltin, IrType::U32, {&constant(static_cast<std::uint32_t>(StageInputKind::BaseInstance)), &constant(0u)}); + IrValue& instance = place(IrOpcode::ISub32, IrType::U32, {&index, &base}); + IrValue& truth = program.CreateValue(IrOpcode::Void, IrType::Bool); + truth.SetImmediateBool(true); + static_cast<void>(place(IrOpcode::SetTessellationAttribute, IrType::Void, {&constant(static_cast<std::uint32_t>(TessellationAttribute::InstanceOutput)), &constant(0u), &instance, &truth})); + } + IrValue& insert(const Access& access, IrOpcode opcode, IrType type, std::initializer_list<IrValue*> arguments) { IrValue& value = program.CreateValue(opcode, type); for (IrValue* argument : arguments) value.AddArgument(argument); diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index d7574e7d416..68bf17d2934 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -108,7 +108,14 @@ RdnaProgram DecodeStageProgram(ShaderStageKind stage, std::span<const std::uint3 ShaderStageInputInfo RequestInputInfo(const RecompileRequest& request) { const auto* mesh = request.graphics && request.graphics->mesh ? &*request.graphics->mesh : nullptr; - const auto* tessellation = request.graphics && request.graphics->tessellation ? &*request.graphics->tessellation : nullptr; + std::optional<TessellationConfiguration> tessellationCopy; + if (request.graphics && request.graphics->tessellation) { + tessellationCopy = *request.graphics->tessellation; + tessellationCopy->instancePatches = TessellationInstancePatches(request); + const auto& capabilities = request.target.supportedCapabilities; + tessellationCopy->drawParameters = std::find(capabilities.begin(), capabilities.end(), static_cast<std::uint32_t>(spv::CapabilityDrawParameters)) != capabilities.end(); + } + const auto* tessellation = tessellationCopy ? &*tessellationCopy : nullptr; return BuildShaderStageInputInfo(toShaderStageKind(request.shader.stage), request.context, HostSubgroupSize(request), mesh, tessellation); } diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 8e77c62bd0e..bcc3e2098e0 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -293,6 +293,8 @@ struct TessellationConfiguration { std::uint32_t patchesPerGroup = 256; std::uint32_t primitiveGroupSize = 0; bool breakAtInstance = false; + std::uint32_t instancePatches = 0; + bool drawParameters = false; }; inline constexpr std::uint32_t MeshDrawPushOffsetBytes = 104; @@ -333,6 +335,11 @@ struct RecompileRequest { bool useCache = true; }; +inline std::uint32_t TessellationInstancePatches(const RecompileRequest& request) { + if (!request.graphics || !request.graphics->tessellation || request.graphics->draw.instanceCount <= 1u || request.graphics->tessellation->breakAtInstance) return 0u; + return request.graphics->draw.indexCount / request.graphics->tessellation->inputControlPoints; +} + enum class DescriptorKind { UniformBuffer, StorageBuffer, diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 0612a023fe0..47bb2d7a101 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -93,7 +93,7 @@ struct SpirvEmitterState { SpirvModule module; const IrProgram& program; ShaderStageInputInfo inputInfo; - std::array<std::uint32_t, 7> tessVariables {}; + std::array<std::uint32_t, 9> tessVariables {}; std::uint32_t tessInnerVariable = 0; SpirvRequirements requirements; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index bd37156052f..5174516920a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -70,6 +70,7 @@ std::uint32_t BuiltInForInput(StageInputKind kind) { case StageInputKind::PrimitiveId: return spv::BuiltInPrimitiveId; case StageInputKind::TessCoord: return spv::BuiltInTessCoord; case StageInputKind::InstanceIndex: return spv::BuiltInInstanceIndex; + case StageInputKind::BaseInstance: return spv::BuiltInBaseInstance; case StageInputKind::FragCoord: return spv::BuiltInFragCoord; case StageInputKind::FrontFacing: return spv::BuiltInFrontFacing; case StageInputKind::HelperInvocation: return spv::BuiltInHelperInvocation; @@ -217,6 +218,7 @@ void DefineInputs(SpirvEmitterState& state) { case StageInputKind::InvocationId: case StageInputKind::PrimitiveId: case StageInputKind::InstanceIndex: + case StageInputKind::BaseInstance: case StageInputKind::Layer: case StageInputKind::SampleId: type = TypeI32(state); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index f4c1da3f2bf..af8a726dfc4 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -140,6 +140,9 @@ void DefineModule(SpirvEmitterState& state) { state.module.EmitCapability(spv::CapabilityComputeDerivativeGroupQuadsKHR); state.module.EmitExtension("SPV_KHR_compute_shader_derivatives"); } + if (std::any_of(state.inputs.begin(), state.inputs.end(), [](const SpirvInputBinding& input) { return input.kind == StageInputKind::BaseInstance; })) { + state.module.EmitCapability(spv::CapabilityDrawParameters); + } const bool fragmentBarycentric = StageOf(state) == IrShaderStage::Pixel && std::any_of(state.inputs.begin(), state.inputs.end(), [](const SpirvInputBinding& input) { return input.perVertex || input.kind == StageInputKind::BaryCoordSmooth || input.kind == StageInputKind::BaryCoordNoPerspective; }); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 395e365f6ea..96c42edc4a9 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -87,7 +87,7 @@ std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std: state.module.AddFunction(spv::OpSelect, TypeU32(state), bits, value, ConstantU32(state, 1u), ConstantU32(state, 0u)); return bits; } - if (kind == StageInputKind::VertexIndex || kind == StageInputKind::InstanceIndex || kind == StageInputKind::InvocationId || kind == StageInputKind::PrimitiveId || kind == StageInputKind::Layer || kind == StageInputKind::SampleId) { + if (kind == StageInputKind::VertexIndex || kind == StageInputKind::InstanceIndex || kind == StageInputKind::BaseInstance || kind == StageInputKind::InvocationId || kind == StageInputKind::PrimitiveId || kind == StageInputKind::Layer || kind == StageInputKind::SampleId) { const auto value = state.module.AllocateId(); const auto bits = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, TypeI32(state), value, variable); @@ -579,9 +579,16 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins if (variable == 0u) { throw std::runtime_error("tessellation attribute has no interface variable"); } - const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput || kind == TessellationAttribute::PatchInput; + const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput || kind == TessellationAttribute::PatchInput || kind == TessellationAttribute::InstanceInput; const auto storage = input ? spv::StorageClassInput : spv::StorageClassOutput; + if (kind == TessellationAttribute::InstanceOutput) { + return variable; + } const auto pointer = state.module.AllocateId(); + if (kind == TessellationAttribute::InstanceInput) { + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, storage, TypeU32(state)), pointer, variable, ConstantU32(state, 0u)); + return pointer; + } if (kind == TessellationAttribute::Factor) { if (!inst.Argument(1)->HasImmediate()) { throw std::runtime_error("tessellation factor index must be immediate"); @@ -629,7 +636,7 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins } void DefineTessellationInterfaces(SpirvEmitterState& state) { - std::array<bool, 7> used {}; + std::array<bool, 9> used {}; for (const IrBlock* block : state.program.BlockOrder()) { for (const IrValue* inst : block->Instructions()) { if (inst->Opcode() != IrOpcode::GetTessellationAttribute && inst->Opcode() != IrOpcode::SetTessellationAttribute) { @@ -650,12 +657,15 @@ void DefineTessellationInterfaces(SpirvEmitterState& state) { continue; } const auto kind = static_cast<TessellationAttribute>(index); - const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput || kind == TessellationAttribute::PatchInput; + const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput || kind == TessellationAttribute::PatchInput || kind == TessellationAttribute::InstanceInput; const bool patch = kind == TessellationAttribute::PatchOutput || kind == TessellationAttribute::PatchInput; const auto storage = input ? spv::StorageClassInput : spv::StorageClassOutput; + const bool instance = kind == TessellationAttribute::InstanceOutput || kind == TessellationAttribute::InstanceInput; std::uint32_t type; if (kind == TessellationAttribute::Factor) { type = array(TypeF32(state), 4u); + } else if (instance) { + type = kind == TessellationAttribute::InstanceInput ? array(TypeU32(state), tess.inputControlPoints) : TypeU32(state); } else if (patch) { type = array(TypeU32Vector(state, 4u), (state.program.Metadata().tessellationPatchStride + 15u) / 16u); } else { @@ -667,8 +677,10 @@ void DefineTessellationInterfaces(SpirvEmitterState& state) { } } auto& variable = state.tessVariables.at(index); - variable = DefineInterfaceVariable(state, type, storage, "tess_attributes"); - if (kind == TessellationAttribute::Factor) { + variable = DefineInterfaceVariable(state, type, storage, instance ? "tess_instance" : "tess_attributes"); + if (instance) { + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationLocation, (state.program.Metadata().tessellationLocalStride + 15u) / 16u); + } else if (kind == TessellationAttribute::Factor) { state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBuiltIn, spv::BuiltInTessLevelOuter); state.tessInnerVariable = DefineInterfaceVariable(state, array(TypeF32(state), 2u), storage, "tess_inner"); state.module.AddAnnotation(spv::OpDecorate, state.tessInnerVariable, spv::DecorationBuiltIn, spv::BuiltInTessLevelInner); diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 3e1f574040e..7fc5176d482 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -195,6 +195,8 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues target.patchesPerGroup = tessellation->patchesPerGroup; target.primitiveGroupSize = tessellation->primitiveGroupSize; target.breakAtInstance = tessellation->breakAtInstance; + target.instancePatches = tessellation->instancePatches; + target.drawParameters = tessellation->drawParameters; } if (stage == ShaderStageKind::Mesh) { if (mesh == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: a mesh-stage program has no mesh configuration"); From 3c17b3dbb24d430a80dd5f0fb82c94e86b6760d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Wed, 14 Oct 2026 19:08:01 +0200 Subject: [PATCH 61/81] docs(debt): update the relative patch index line for instanced draws --- docs/dev/debt/shader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 92d3cea3b1f..7392cc8399a 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -104,7 +104,7 @@ - [VGT_HOS_MIN/MAX_TESS_LEVEL](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) clamp positive tessellation factors (inner ones to at least 1); outer factors that are zero, negative or NaN still cull the patch and such inner factors become max(HOS_MIN, 1). The console's clamp and cull order is not measured. - [Patch constants](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) that hull programs write and domain programs read are placed by their off-chip address modulo the record stride, as the two compile apart; a domain read of a field the hull never writes, or of only a neighbouring patch's record, is not detected. - [Integer partitioning](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (`VGT_TF_PARAM.PARTITIONING` 0) is drawn with Vulkan's equal spacing, which [radeonsi programs as `PART_INTEGER`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L539-540), with factors bounded at 64 instead of fractional-odd's 63; how the console rounds a clamped factor up to a whole number of segments is not measured. Power-of-two (1) and fractional-even (3) partitioning throw. -- The [relative patch index](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) that hull and domain programs use as data is (PrimitiveID mod PRIM_GRP_SIZE) mod NUM_PATCHES, as if each primitive group and each instance started new threadgroups; it throws when GE_CNTL.BREAK_WAVE_AT_EOI is clear. Unmeasured; PPSA26344 programs 64 primitives for 63 patches. +- The [relative patch index](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) that hull and domain programs use as data is (PrimitiveID mod PRIM_GRP_SIZE) mod NUM_PATCHES, with the draw-relative instance times the patches per instance added when GE_CNTL.BREAK_WAVE_AT_EOI is clear and the instances share hull threadgroups; a domain that uses it in such a draw, and a hull without shaderDrawParameters, throw. Unmeasured on hardware; PPSA26344 programs 64 primitives for 63 patches. - The [output modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) (`mul:2`, `mul:4`, `div:2`) follows the shader's float mode as measured on RDNA2 (gfx1035): the hardware applies it only with IEEE mode off and output denormals of the result's precision flushed (bit 1 of the `FLOAT_MODE` denormal field clear), and ignores it otherwise. On f32 results it is applied as the hardware does with f32 denormals flushed; a shader without a known float mode is translated with IEEE mode off and f32 denormals flushed, the mode titles use. On f16 and f64 results it is ignored, and it throws in the modes where the hardware applies it. - The [clamp modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows `DX10_CLAMP`, measured on RDNA2 (gfx1035) for f32, f16, packed f16 and f64 results at IEEE=0 and IEEE=1: with `DX10_CLAMP`=1 (and in shaders without a known float mode) a NaN result clamps to +0, including the quieted NaN that `v_pk_min_f16` and `v_pk_max_f16` return for a signaling NaN operand with IEEE=1 (measured on RDNA2 gfx1036 at both `IEEE_MODE` and both `DX10_CLAMP` settings); with `DX10_CLAMP`=0 a NaN result is written unchanged and every other value clamps to [0, 1] as before. f64 results follow the same rule on hardware, but a clamped f64 result with `DX10_CLAMP`=0 throws until it has an execution test. With `DX10_CLAMP`=0 the NaN's bits are those the translation already produces for the instruction without clamp, so where those are left to the driver (f32 and scalar f16 arithmetic) the payload is the driver's, not the hardware's input propagation. `v_dot2_f32_f16` and the integer clamps are unchanged. - [FP16_OVFL](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows the shader's float mode as measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f16 denormals kept and flushed: with `FP16_OVFL`=1 an f16 result that rounds to ±infinity is written as ±0x7bff unless a source operand is infinite, so `v_rcp_f16`, `v_rsq_f16` and `v_log_f16` of ±0 and `v_exp_f16` of 16 or more are clamped while an infinite input passes through, and `v_div_fixup_f16` keeps the infinity it writes for a zero denominator or an infinite numerator and clamps any other infinite quotient, a NaN one included. `v_cvt_pkrtz_f16_f32` rounds toward zero and never overflows. The sources are checked as the f32 values the translation computes with; the rule is applied to `v_fma_mix*_f16` with f32 sources and to `v_interp_p2_f16` without a measurement of infinite f32 sources or parameters. Round modes other than nearest even are not modelled, so the overflow threshold is the nearest-even one (65520). From c280018b0b1c7d5609c268b0d697a980186870e4 Mon Sep 17 00:00:00 2001 From: MarcZX100 <marczxrr@gmail.com> Date: Mon, 12 Oct 2026 21:21:11 +0000 Subject: [PATCH 62/81] fix(agc): treat type-0 texture descriptors as null --- .../Graphics/include/GuestTextureResource.hpp | 1 + .../Graphics/src/GuestTextureResource.cpp | 7 +++++++ .../Graphics/src/ShaderResources.cpp | 16 ++++++++-------- .../tests/GuestTextureResource.cpp | 8 ++++++++ 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp index 997e9dc7e74..09df2e15b73 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp @@ -90,6 +90,7 @@ struct GuestTextureResource { float EffectiveMinLod(const GuestTextureResource& resource); +bool IsNullTextureResource(std::span<const std::uint32_t> words); GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words); bool DescriptorSingleLevel(std::span<const std::uint32_t> words); bool MatchesGuestDimension(ShaderRecompiler::DescriptorImageShape shape, TextureDimension dimension); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 55ecd30e305..e9b1ec19a26 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -50,6 +50,13 @@ TextureDimension resolveDimension(std::uint32_t raw) { } +bool IsNullTextureResource(std::span<const std::uint32_t> words) { + Require(words.size() == 8, "guest texture descriptor must contain 8 dwords"); + const auto base40 = (static_cast<std::uint64_t>(words[0]) | (static_cast<std::uint64_t>(words[1]) << 32u)) & 0xffffffffffull; + const auto type = (words[3] >> 28u) & 0xfu; + return base40 == 0 || type == 0; +} + GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words) { Require(words.size() == 8, "guest texture descriptor must contain 8 dwords"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index e89fa2b31da..3e8b44de0e0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2671,7 +2671,7 @@ ShaderResources::OwnRefreshFallback ShaderResources::refreshOwnObjects(std::span if (binding.kind == ShaderRecompiler::DescriptorKind::SampledImage) { for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * 8u, 8u); - if (words[0] == 0u && (words[1] & 0xffu) == 0u) continue; + if (IsNullTextureResource(words)) continue; if (textureIndex >= textures.size()) return OwnRefreshFallback::Uncached; const auto index = textureIndex++; const auto overlap = listed(index, false); @@ -2681,7 +2681,7 @@ ShaderResources::OwnRefreshFallback ShaderResources::refreshOwnObjects(std::span } else if (binding.kind == ShaderRecompiler::DescriptorKind::StorageImage) { for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * 8u, 8u); - if (words[0] == 0u && (words[1] & 0xffu) == 0u) continue; + if (IsNullTextureResource(words)) continue; if (storageIndex >= storageTextures.size()) return OwnRefreshFallback::Uncached; const auto index = storageIndex++; if (listed(index, true) == overlapping.end()) continue; @@ -2769,7 +2769,7 @@ bool ShaderResources::Revalidate(std::span<const CompiledShader> shaders, ProofR const auto elementWords = binding.count != 0 ? binding.guestDescriptor.size() / binding.count : 0; for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * elementWords, elementWords); - if (words[0] == 0u && (words[1] & 0xffu) == 0u) continue; + if (IsNullTextureResource(words)) continue; const auto resource = DecodeTextureResource(words); const VkComponentMapping components = ViewComponents(resource); if (textureIndex >= textures.size() || cachedTexture(context, words, resource, components, 0, !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element)) != textures[textureIndex]) return false; @@ -2778,7 +2778,7 @@ bool ShaderResources::Revalidate(std::span<const CompiledShader> shaders, ProofR } else if (binding.kind == ShaderRecompiler::DescriptorKind::StorageImage) { for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * 8u, 8u); - if (words[0] == 0u && (words[1] & 0xffu) == 0u) continue; + if (IsNullTextureResource(words)) continue; if (storageIndex >= storageTextures.size()) return false; std::shared_ptr<StorageTexture> expected; if (SameAsPreviousStorageElement(binding, element) && StorageDedupeEnabled()) expected = storageTextures[storageIndex - 1]; @@ -3754,7 +3754,7 @@ bool ShaderResources::precollectImages() { } ImageRecord record; record.sampled = sampled; - if (words[0] != 0u || (words[1] & 0xffu) != 0u) { + if (!IsNullTextureResource(words)) { const auto* memo = words.size() == 8 ? &decodedImage(context.device, words) : nullptr; record.resource = memo != nullptr ? memo->resource : DecodeTextureResource(words); record.guestBytes = memo != nullptr ? memo->guestBytes : DescribeSurface(record.resource).guestBytes; @@ -3863,7 +3863,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * elementWords, elementWords); const auto* record = nextRecord(); - if (words[0] == 0u && (words[1] & 0xffu) == 0u) { + if (IsNullTextureResource(words)) { item.imageAllocations.push_back(std::numeric_limits<std::size_t>::max()); --plannedSampledImages; continue; @@ -3932,7 +3932,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind if (sameAsPrevious) ++mipOffset; else mipOffset = 0; const auto* record = nextRecord(); - if (words[0] == 0u && (words[1] & 0xffu) == 0u) { + if (IsNullTextureResource(words)) { item.imageAllocations.push_back(std::numeric_limits<std::size_t>::max()); --plannedStorageImages; continue; @@ -4267,7 +4267,7 @@ std::vector<std::pair<std::uint64_t, std::uint64_t>> ShaderResources::PresyncSur // Stage A has not run (an address-based build, or the pass is off): decoded from the shader. if (deferredCompute.program == nullptr) return surfaces; forEachImageElement(*deferredCompute.program, [&](const ShaderRecompiler::DescriptorBinding& binding, std::uint32_t, std::span<const std::uint32_t> words) { - if (words[0] == 0u && (words[1] & 0xffu) == 0u) return; + if (IsNullTextureResource(words)) return; const auto resource = DecodeTextureResource(words); consider(resource.format, resource.baseAddress, DescribeSurface(resource).guestBytes, binding.kind == ShaderRecompiler::DescriptorKind::SampledImage); }); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index bd5e36614f3..eba87022087 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -96,6 +96,14 @@ void rejectFields(const Fields& f, std::string_view reason) { void RunGuestTextureResourceTests() { Fields base; + const auto baseWords = pack(base); + Require(!IsNullTextureResource(baseWords), "a nonzero 2D texture was classified as null"); + Fields nullBase = base; + nullBase.base40 = 0; + Require(IsNullTextureResource(pack(nullBase)), "a zero-base texture was not classified as null"); + Fields nullType = base; + nullType.typeRaw = 0; + Require(IsNullTextureResource(pack(nullType)), "a type-0 texture with a nonzero base was not classified as null"); auto result = DecodeTextureResource(pack(base)); Require(result.baseAddress == (base.base40 << 8u), "decoded base address changed"); Require(result.width == 16 && result.height == 16, "decoded width or height changed"); From 58b2245027222137fe5a26dbc11267a0c942c98e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Wed, 14 Oct 2026 19:14:08 +0200 Subject: [PATCH 63/81] docs(debt): record type-0 texture descriptors bound as null images Co-Authored-By: Claude <noreply@anthropic.com> --- 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 979a7bfa7e7..ffa3682cbf2 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -30,6 +30,7 @@ ### Functional - [Initial stencil state](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) uses zero for `DB_STENCIL_CONTROL`, `DB_STENCILREFMASK` and `DB_STENCILREFMASK_BF` (context `0x10b`-`0x10d`), including after `CLEAR_STATE`. The public V0, V4, V5 and V7-V11 [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) supply these values. Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h) agrees on the control register but uses `0x01000000` for the reference masks; the console reset values have not been measured. Planet Coaster (PPSA01736 v01.017) enables stencil tests without writing the control register. +- [Type-0 texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (`TYPE` = 0 in dword 3) with a nonzero base are bound as null images, like a zero base. The behaviour comes from a gfx1036 (RDNA2) measurement in issue #4139 (image sample, load and resinfo return zero, no fault); it was not measured on the console's gfx1013. - [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. Retired Vulkan images remain allocated until device teardown to preserve outstanding references. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported. - [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane, or of a later slice, is a storage image over the slices from 0 to its last (`LayeredDepthStorage`); only the slices from its base slice to its last are copied: a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, base slice, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, a view from another base slice, or a sampled copy) is a separate image, copied in before and out after each use, so views that are written before a flush and share a slice do not merge: the later copy-out wins, so their results are not merged. A matching view still throws when it is a cube, a 2D array of more than one layer of the stencil plane or from a later slice, or another level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. From 204c2ac4af24810a216eb651a82896ace63eabbb Mon Sep 17 00:00:00 2001 From: Nicolas Falesy <112726756+nicolasfalesy@users.noreply.github.com> Date: Mon, 12 Oct 2026 23:22:49 -0400 Subject: [PATCH 64/81] refactor(libSceSaveData.native): name sceSaveDataPrepareForTransferring The NID RvID8BVeg+g, declared as sceSaveDataUnknown_RvID8BVeg for Minecraft (PPSA17221), hashes to sceSaveDataPrepareForTransferring. The name is not in aerolib; it was found by combining words from the libSceSaveData names that are. Behaviour is unchanged: it still throws, now naming the real function. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017WeVmjnnPPKdxVTpr2Vjtw --- core/libs/prx/libSceSaveData.native/Export.cpp | 6 +++--- docs/dev/debt/libSceSaveData.native.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 00529793b72..349dd9a4ee5 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -893,9 +893,9 @@ int APS5_VABI sceSaveDataDirNameSearch2(const SaveDataDirNameSearchCond* cond, S return rc; } -APS5_EXPORT("RvID8BVeg+g", sceSaveDataUnknown_RvID8BVeg); -int APS5_VABI sceSaveDataUnknown_RvID8BVeg() { - NotImplemented_nid_no_patch("RvID8BVeg+g"); +APS5_EXPORT("RvID8BVeg+g", sceSaveDataPrepareForTransferring); +int APS5_VABI sceSaveDataPrepareForTransferring() { + NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index 6759fd2a9e9..ca4be61ba75 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceSaveDataUnknown_RvID8BVeg](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native, NID `RvID8BVeg+g`) - declared without parameters as a throwing stub; Minecraft (PPSA17221) imports it beside `sceSaveDataTransferringMountPs4` and `sceSaveDataDirNameSearchPs4` and fails to load without it. No name in aerolib or among the PS4-migration variants tried with `tools/nid_names.py` hashes to it. +- [sceSaveDataPrepareForTransferring](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native, NID `RvID8BVeg+g`) - declared without parameters as a throwing stub; Minecraft (PPSA17221) imports it beside `sceSaveDataTransferringMountPs4` and `sceSaveDataDirNameSearchPs4` and fails to load without it. The name hashes to the NID but is not in aerolib; it was found by combining words from the libSceSaveData names in aerolib. Its arguments and behaviour are unknown. - [sceSaveDataTransferringMountPs4, sceSaveDataDirNameSearchPs4](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - arguments assumed to be those of `sceSaveDataTransferringMount` and `sceSaveDataDirNameSearch`: KytyPS5 binds both NIDs to the PS5 functions, and [prosper](https://github.com/mattias800/prosper) captured the same result layout on a title for the search. There is no PS4 save area, so the mount returns `NOT_FOUND` and the search reports zero hits - [sceSaveDataConvert](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the parameter block (user id at `+0x00`, `SceSaveDataDirName` pointer at `+0x08`, 48 opaque bytes after it) is read from PPSA28416's single call, not confirmed by KytyPS5, shadPS4, fpPS4 or SharpProspero, which have no body for it. Host saves have no size limit, so an existing save returns `SCE_OK` without touching its files and no event is queued; what the console returns for a save that is mounted, already converted or needs no conversion is unknown. The directory name is validated as `sceSaveDataDelete` does - [sceSaveDataGetConvertProgress](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - imported by PPSA16064; signature and error codes follow the [SDK 12.000 reference](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-get-convert-progress.md) (`float* progress`). Save data conversion on host filesystems completes synchronously, so the progress query returns `SAVE_DATA_OK` with the last conversion's progress (`0.0f` before any conversion or when conversion fails, `1.0f` after successful conversion), `SAVE_DATA_ERROR_PARAMETER` if `progress` is null, or `SAVE_DATA_ERROR_NOT_INITIALIZED` when the library is uninitialized; intermediate asynchronous conversion progress on console is unmeasured From 31e53e53d7d48732f1237145725f1f165e254901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Wed, 14 Oct 2026 19:17:47 +0200 Subject: [PATCH 65/81] fix(libSceSaveData.native): drop the duplicate APS5_EXPORT of the real name --- core/libs/prx/libSceSaveData.native/Export.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 349dd9a4ee5..bb58612ea7b 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -893,7 +893,6 @@ int APS5_VABI sceSaveDataDirNameSearch2(const SaveDataDirNameSearchCond* cond, S return rc; } -APS5_EXPORT("RvID8BVeg+g", sceSaveDataPrepareForTransferring); int APS5_VABI sceSaveDataPrepareForTransferring() { NotImplemented_nid_no_patch(__func__); return 0; From 5dcf28d36ceb3102eab7b012f211dbb3b5757cdf Mon Sep 17 00:00:00 2001 From: Nicolas Falesy <112726756+nicolasfalesy@users.noreply.github.com> Date: Tue, 13 Oct 2026 00:29:01 -0400 Subject: [PATCH 66/81] fix(libkernel): answer fcntl status flags on libSceNet sockets With libSceNet sockets on ordinary descriptor numbers (#4391), a title that makes a libSceNet socket non-blocking with plain fcntl(F_GETFL) and fcntl(F_SETFL, O_NONBLOCK) reached File::Fcntl, whose classification throws for a socket. libSceNet now registers a status control with libkernel, like its option setter: F_GETFL returns O_RDWR with the socket's non-blocking flag, F_SETFL sets it through SO_NBIO, as fcntl already does for libkernel sockets. ProsperoLight (PPSA99002) stopped here on Linux. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017WeVmjnnPPKdxVTpr2Vjtw --- core/libs/prx/libSceNet/Export.cpp | 35 +++++++++++++++++++ .../libkernel/Socket/include/SocketPoll.hpp | 3 ++ core/libs/prx/libkernel/Socket/src/Socket.cpp | 10 ++++++ core/libs/prx/libkernel/Socket/src/Udp.cpp | 5 +++ core/libs/tests/GuestSceNet.cpp | 22 ++++++++++++ 5 files changed, 75 insertions(+) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index e03f88e831d..95bb7c755ef 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -595,6 +595,41 @@ bool SetSocketOptionForKernel(int descriptor, int level, int option, const void* const bool g_socketOptionSetterRegistered = (KernelSetSocketOptionSetter_nid_no_patch(&SetSocketOptionForKernel), true); +constexpr int GuestFcntlGetStatus = 3; +constexpr int GuestFcntlSetStatus = 4; +constexpr int GuestReadWrite = 2; +constexpr int GuestNonblocking = 4; +constexpr int GuestStatusFlagsMask = 7; + +bool ControlSocketStatusForKernel(int descriptor, int command, int flags, int* result, int* error) { + bool nonblock = false; + { + std::lock_guard lk(g_mutex); + const auto it = g_socks.find(descriptor); + if (it == g_socks.end()) return false; + nonblock = it->second.nonblock; + } + *result = 0; + *error = 0; + if (command == GuestFcntlGetStatus) { + *result = GuestReadWrite | (nonblock ? GuestNonblocking : 0); + return true; + } + if (command != GuestFcntlSetStatus) { + *error = NET_EINVAL; + return true; + } + if ((flags & ~GuestStatusFlagsMask) != 0) { + *error = NET_EOPNOTSUPP; + return true; + } + const int enabled = (flags & GuestNonblocking) != 0 ? 1 : 0; + if (sceNetSetsockopt(descriptor, NET_SOL_SOCKET, NET_SO_NBIO, &enabled, sizeof(enabled)) != 0) *error = *errno_slot(); + return true; +} + +const bool g_socketStatusControlRegistered = (KernelSetSocketStatusControl_nid_no_patch(&ControlSocketStatusForKernel), true); + int APS5_VABI sceNetInit_nid_postfix(void) { std::lock_guard lk(g_mutex); g_net_inited = true; diff --git a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp index 622a17bd20a..227cce5ba0a 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp @@ -18,6 +18,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 StatusControl = bool (*)(int descriptor, int command, int flags, int* result, int* error); } extern "C" void KernelSetSocketPoller_nid_no_patch(KernelSocketPoll::Poller poller); @@ -25,5 +26,7 @@ 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 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" int KernelReserveSocketNumber_nid_no_patch(std::uintptr_t native); extern "C" void KernelReleaseSocketNumber_nid_no_patch(int number); diff --git a/core/libs/prx/libkernel/Socket/src/Socket.cpp b/core/libs/prx/libkernel/Socket/src/Socket.cpp index e01a8b7f34d..8e7907b77c0 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -35,6 +35,7 @@ struct GuestTimeval { std::atomic<KernelSocketPoll::Poller> g_socketPoller{nullptr}; std::atomic<KernelSocketPoll::Closer> g_socketCloser{nullptr}; std::atomic<KernelSocketPoll::OptionSetter> g_socketOptionSetter{nullptr}; +std::atomic<KernelSocketPoll::StatusControl> g_socketStatusControl{nullptr}; bool DescriptorSet(const void* set, int descriptor) { return set != nullptr && ((static_cast<const std::uint64_t*>(set)[descriptor / 64] >> (descriptor % 64)) & 1u) != 0; @@ -153,6 +154,15 @@ bool KernelSetSocketOption_nid_no_patch(int descriptor, int level, int option, c return setter != nullptr && setter(descriptor, level, option, value, length, error); } +void KernelSetSocketStatusControl_nid_no_patch(KernelSocketPoll::StatusControl control) { + g_socketStatusControl.store(control); +} + +bool KernelControlSocketStatus_nid_no_patch(int descriptor, int command, int flags, int* result, int* error) { + const auto control = g_socketStatusControl.load(); + return control != nullptr && control(descriptor, command, flags, result, error); +} + int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout) { const auto* limit = static_cast<const GuestTimeval*>(timeout); if (nfds < 0 || nfds > SelectDescriptorLimit || (limit != nullptr && (limit->seconds < 0 || limit->microseconds < 0 || limit->microseconds >= 1000000))) { diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 74deec5697d..435d75c4d75 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -755,6 +755,11 @@ int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { va_end(arguments); #endif } + int result = 0; + int netError = 0; + if (KernelControlSocketStatus_nid_no_patch(descriptor, command, static_cast<int>(argument), &result, &netError)) { + return netError == 0 ? result : Fail(netError); + } return File::Fcntl(descriptor, command, argument); } const auto socket = Lookup(descriptor); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index c117778e49e..d4f635b46a2 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -74,6 +74,7 @@ int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI close_nid_postfix(int); int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); +int APS5_VABI fcntl_nid_postfix(int, int, ...); int* APS5_VABI __error_nid_postfix(); int APS5_VABI sceNetInetPton(int, const char*, void*); const char* APS5_VABI sceNetInetNtop(int, const void*, char*, std::uint32_t); @@ -506,6 +507,26 @@ static void CheckKernelTimeouts() { Require(sceNetSocketClose(descriptor) == 0); } +static void CheckKernelStatusFlags() { + const int descriptor = sceNetSocket("kernel-status", 2, 1, 6); + Require(descriptor >= 0); + Require(fcntl_nid_postfix(descriptor, 3) == 2); + Require(fcntl_nid_postfix(descriptor, 4, 2 | 4) == 0); + Require(fcntl_nid_postfix(descriptor, 3) == (2 | 4)); + int enabled = 0; + std::uint32_t length = sizeof(enabled); + Require(sceNetGetsockopt(descriptor, 0xffff, 0x1200, &enabled, &length) == 0 && enabled == 1); + Require(fcntl_nid_postfix(descriptor, 4, 2) == 0); + Require(fcntl_nid_postfix(descriptor, 3) == 2); + length = sizeof(enabled); + Require(sceNetGetsockopt(descriptor, 0xffff, 0x1200, &enabled, &length) == 0 && enabled == 0); + *__error_nid_postfix() = 0; + Require(fcntl_nid_postfix(descriptor, 4, 0x100) == -1 && *__error_nid_postfix() == 45); + *__error_nid_postfix() = 0; + Require(fcntl_nid_postfix(descriptor, 1) == -1 && *__error_nid_postfix() == 22); + Require(close_nid_postfix(descriptor) == 0); +} + static void CheckAddressText(int family, const char* text) { std::array<std::uint8_t, 16> address{}; Require(sceNetInetPton(family, text, address.data()) == 1); @@ -844,6 +865,7 @@ int main() { Require(Failed(sceNetSocketClose(closed), 9)); CheckKernelTimeouts(); CheckSocketError(); + CheckKernelStatusFlags(); const int listener = sceNetSocket(nullptr, 2, 1, 6); Require(listener >= 0); From 5bf18f0d71df4a7b3c99461ab7b556c7b1ef777c Mon Sep 17 00:00:00 2001 From: Chad <chadmaster52@gmail.com> Date: Wed, 14 Oct 2026 12:21:19 -0500 Subject: [PATCH 67/81] perf(libc): remember clean pages once per run in Windows write collects Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../libs/prx/libc/include/WindowsMappings.hpp | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 8df63459a7a..acff2b7f511 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -334,6 +334,12 @@ class WindowsMappings { } } else if (clear && !collectable(address, end)) return false; auto position = views.lower_bound(address & ~(pageBytes - 1)); + std::uintptr_t cleanStart = 0; + std::uintptr_t cleanEnd = 0; + const auto flushClean = [&] { + if (cleanEnd > cleanStart) rememberClean(cleanStart, cleanEnd); + cleanStart = cleanEnd = 0; + }; for (auto cursor = address; cursor < end;) { const auto nextClean = cleanRanges.upper_bound(cursor); if (nextClean != cleanRanges.begin()) { @@ -346,6 +352,7 @@ class WindowsMappings { const auto base = cursor & ~(pageBytes - 1); const auto found = viewAt(base, position); if (found != views.end() && found->second.resident) { + flushClean(); if (!watchResident) return false; const auto available = capacity - *count; if (available == 0) return true; @@ -371,7 +378,10 @@ class WindowsMappings { } } } - if (view.protection == PAGE_NOACCESS) return false; + if (view.protection == PAGE_NOACCESS) { + flushClean(); + return false; + } const bool pinned = view.page->pins != 0; std::uint8_t settled = 0; auto settledEnd = cursor; @@ -414,24 +424,25 @@ class WindowsMappings { other.armed |= arming; } const auto tracked = static_cast<std::uint8_t>(settled & ~untracked); - auto cleanFrom = cursor; - auto cleanTo = cursor; for (std::size_t sub = 0; sub < subpages; ++sub) { if ((tracked & (1u << sub)) == 0) continue; const auto from = std::max(cursor, base + sub * subpageBytes); const auto to = std::min(settledEnd, base + (sub + 1) * subpageBytes); if (from == base + sub * subpageBytes && to == base + (sub + 1) * subpageBytes) view.seen[sub] = view.page->generation[sub]; - if (from != cleanTo) { - if (cleanFrom != cleanTo) rememberClean(cleanFrom, cleanTo); - cleanFrom = from; + if (from != cleanEnd) { + flushClean(); + cleanStart = from; } - cleanTo = to; + cleanEnd = to; } - if (cleanFrom != cleanTo) rememberClean(cleanFrom, cleanTo); } - if (full) return true; + if (full) { + flushClean(); + return true; + } cursor = stop; } else { + flushClean(); if (collection == Collection::Fresh) { const auto next = views.lower_bound(cursor); cursor = next == views.end() ? end : std::min(end, next->first); @@ -450,6 +461,7 @@ class WindowsMappings { cursor = stop; } } + flushClean(); return true; } From 6b480cc2be64f326537d78321d21ce4f97ecc48d Mon Sep 17 00:00:00 2001 From: Gustavo Ariel Gamarra Rojas <111788638+GustavoGamarra95@users.noreply.github.com> Date: Tue, 13 Oct 2026 16:11:47 -0300 Subject: [PATCH 68/81] fix(agc): copy a depth surface's plane through the texture's padded linear slices The plane copy laid its staging buffer out as tight slices while the detiler writes the guest's linear layout, which pads rows and slices: a 32x32 D16 cube in a 64 KiB swizzle has 16384-byte slices, not 2048, and a 128x128 one has 512-byte rows and 64 KiB slices. Each region in record now takes the layer's LinearLayerOffset plus the mip's linear offset and its pitch as the row length, for both the slices copied from a live depth surface and the ones detiled from guest memory. The staging buffer grows to the padded size, keeping the buffer it replaces alive for copies already recorded, and a mismatch says which geometry it saw. --- .../Graphics/src/DepthSurface.cpp | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index ce18ed43846..a16d0d64c40 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -316,7 +316,8 @@ class DepthPlaneCopy { allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); Check(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory depth plane copy"); Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory depth plane copy"); - staging = std::make_unique<DeviceBuffer>(context, static_cast<std::size_t>(sliceBytes()) * layers, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + stagingSize = static_cast<std::size_t>(sliceBytes()) * layers; + staging = std::make_unique<DeviceBuffer>(context, stagingSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); } catch (...) { release(); throw; @@ -329,7 +330,17 @@ class DepthPlaneCopy { std::shared_ptr<Texture> Refresh(std::span<const DepthPlaneSlice> slices, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType, std::uint32_t baseLayer) { Require(slices.size() == layers, "depth plane copy slices do not match its layers"); const auto geometry = DescribeSurface(resource); - Require(geometry.layers == layers && geometry.sliceLinearBytes == sliceBytes() && !geometry.mips.empty(), "depth plane copy geometry does not match its layers"); + if (geometry.layers != layers || geometry.sliceLinearBytes < sliceBytes() || geometry.mips.empty()) { + char text[320]; + std::snprintf(text, sizeof(text), "AGC graphics: depth plane copy geometry does not match its layers (copy %ux%u, %u layers, %llu bytes a slice; texture %ux%u guest format %u, dimension %d, tile mode %u, %u layers, %llu bytes a slice, %zu mips)", extent.width, extent.height, layers, static_cast<unsigned long long>(sliceBytes()), resource.width, resource.height, resource.format, static_cast<int>(resource.dimension), resource.tileMode, geometry.layers, static_cast<unsigned long long>(geometry.sliceLinearBytes), geometry.mips.size()); + throw std::runtime_error(text); + } + const auto stagingBytes = static_cast<std::size_t>(geometry.sliceLinearBytes) * layers; + if (stagingBytes > stagingSize) { + retiredStaging.push_back(std::move(staging)); + staging = std::make_unique<DeviceBuffer>(context, stagingBytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + stagingSize = stagingBytes; + } std::vector<std::shared_ptr<Buffer>> uploads; std::vector<std::uint32_t> uploadLayers; std::vector<std::uint32_t> copiedLayers; @@ -382,6 +393,8 @@ class DepthPlaneCopy { VkImage image = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; std::unique_ptr<DeviceBuffer> staging; + std::size_t stagingSize = 0; + std::vector<std::unique_ptr<DeviceBuffer>> retiredStaging; std::map<std::array<std::uint32_t, 6>, std::shared_ptr<Texture>> textures; std::vector<GuestLayer> guestLayers; std::vector<DepthPlaneSlice> surfaceLayers; @@ -399,9 +412,11 @@ class DepthPlaneCopy { const auto barrier = context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier"); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); std::vector<VkBufferImageCopy> regions; + const auto& mip = geometry.mips.front(); const auto region = [&](std::uint32_t layer, VkImageAspectFlags aspectMask, std::uint32_t imageLayer) { VkBufferImageCopy copy{}; - copy.bufferOffset = sliceBytes() * layer; + copy.bufferOffset = geometry.LinearLayerOffset(layer) + mip.linearOffset; + copy.bufferRowLength = mip.pitchBytes / (format == VK_FORMAT_D32_SFLOAT ? 4u : 2u); copy.imageSubresource = {aspectMask, 0, imageLayer, 1}; copy.imageExtent = {extent.width, extent.height, 1}; return copy; From ad5073432747a101cac2835c94a6d424b0512449 Mon Sep 17 00:00:00 2001 From: Gustavo Ariel Gamarra Rojas <111788638+GustavoGamarra95@users.noreply.github.com> Date: Tue, 13 Oct 2026 16:11:47 -0300 Subject: [PATCH 69/81] test(agc): sample a small D16 cube whose linear slices are padded A 32x32 D16 depth surface read as a six-face cube through a depth-bits R16_UINT view: its faces are narrower than a 64 KiB block, so the texture's linear slices are 16384 bytes, not the plane's 2048. Face 0 comes from the live surface, faces 1-5 from guest memory. The draw helper now sizes its color target with ColorTargetLayout so the small target is accepted. --- .../tests/execution/DepthPlaneSampling.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp index 7fb674a05e6..02d8ccde911 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" @@ -61,6 +62,7 @@ constexpr std::size_t IntegerOffset = 0xa0000; constexpr std::size_t DepthBitsOffset = 0xb0000; constexpr std::size_t OneSliceOffset = 0xd0000; constexpr std::size_t FirstSliceOffset = 0xe0000; +constexpr std::size_t SmallCubeOffset = 0x180000; constexpr float DrawnDepth = 0.25f; constexpr float DrawnD16Depth = 0.2f; constexpr std::uint32_t DrawnD16Bits = 13107; @@ -206,7 +208,7 @@ void DrawDepth(AgcDriver::VulkanDevice& device, std::uint64_t address, VkExtent2 }}; AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; - state.color = {reinterpret_cast<std::uintptr_t>(Pixels.data()), extent, VK_FORMAT_R8G8B8A8_UNORM, static_cast<std::size_t>(extent.width) * extent.height * 4u, 0xe4u}; + state.color = {reinterpret_cast<std::uintptr_t>(Pixels.data()), extent, VK_FORMAT_R8G8B8A8_UNORM, AgcDriver::Graphics::ColorTargetLayout(extent.width, extent.height, AgcDriver::Graphics::ColorTileMode::Linear).Bytes(), 0xe4u}; state.colors = {state.color}; state.hasColorTarget = true; state.depth = AgcDriver::Graphics::DepthTarget{address, 0, extent, format, 1.0f, 0}; @@ -333,6 +335,17 @@ void FirstSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::spa Expect(output, Bits(DrawnDepth), "3-slice D32 2D array through a 2D shader image"); } +void SmallD16CubeCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span<std::uint32_t> output) { + auto* base = block.At(SmallCubeOffset); + DrawDepth(device, reinterpret_cast<std::uintptr_t>(base), {32, 32}, VK_FORMAT_D16_UNORM, DrawnD16Depth); + for (std::uint32_t face = 1; face < 6u; ++face) FillSlice(base + face * SliceBytes, SliceBytes, 0x1230u + face, 2u); + const Shape shape{Format16Uint, 32, 32, TypeCube, 0, Swizzle64KbZX}; + for (std::uint32_t face = 0; face < 6u; ++face) { + Load(device, base, shape, face, output); + Expect(output, face == 0 ? DrawnD16Bits : 0x1230u + face, "small D16 cube face " + std::to_string(face)); + } +} + void BaseSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span<std::uint32_t> output) { auto* base = block.At(ArrayOffset); Shape shape{Format32Float, 128, 128, Type2DArray, 2, Swizzle64KbZX}; @@ -377,6 +390,7 @@ int main() { DepthBitsArrayCase(*device, block, output); DepthBitsBaseSliceCase(*device, block, output); FirstSliceCase(*device, block, output); + SmallD16CubeCase(*device, block, output); BaseSliceCase(*device, block, output); RefusalCase(*device, block); std::puts("depth plane sampling tests passed"); From 86c66916a6c5f89295122c21f783f83f18076b8f Mon Sep 17 00:00:00 2001 From: alfalfa-star <kosher-chive-take@duck.com> Date: Tue, 13 Oct 2026 16:11:47 -0300 Subject: [PATCH 70/81] test(agc): sample a 128x128 D16 cube whose detiled rows are padded Taken from #4722 (Kena: Bridge of Spirits, PPSA01802): its point-light shadow maps are 128x128 D16 cubes read through an R16 view in a 64 KiB Z swizzle, whose block holds 256x128 texels of 16 bits, so each detiled row is 512 bytes and each face 64 KiB instead of 256 bytes and 32 KiB. Face 0 comes from the live surface, faces 1-5 from guest memory. --- .../tests/execution/DepthPlaneSampling.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp index 02d8ccde911..9b74cf6f3a1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp @@ -62,6 +62,7 @@ constexpr std::size_t IntegerOffset = 0xa0000; constexpr std::size_t DepthBitsOffset = 0xb0000; constexpr std::size_t OneSliceOffset = 0xd0000; constexpr std::size_t FirstSliceOffset = 0xe0000; +constexpr std::size_t PaddedCubeOffset = 0x120000; constexpr std::size_t SmallCubeOffset = 0x180000; constexpr float DrawnDepth = 0.25f; constexpr float DrawnD16Depth = 0.2f; @@ -335,6 +336,17 @@ void FirstSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::spa Expect(output, Bits(DrawnDepth), "3-slice D32 2D array through a 2D shader image"); } +void PaddedCubeCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span<std::uint32_t> output) { + auto* base = block.At(PaddedCubeOffset); + DrawDepth(device, reinterpret_cast<std::uintptr_t>(base), {128, 128}, VK_FORMAT_D16_UNORM, DrawnD16Depth); + for (std::uint32_t face = 1; face < 6u; ++face) FillSlice(base + face * SliceBytes, SliceBytes, 0x1000u + face, 2u); + const Shape shape{Format16Uint, 128, 128, TypeCube, 0, Swizzle64KbZX}; + for (std::uint32_t face = 0; face < 6u; ++face) { + Load(device, base, shape, face, output); + Expect(output, face == 0 ? DrawnD16Bits : 0x1000u + face, "D16 cube with padded rows, face " + std::to_string(face)); + } +} + void SmallD16CubeCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span<std::uint32_t> output) { auto* base = block.At(SmallCubeOffset); DrawDepth(device, reinterpret_cast<std::uintptr_t>(base), {32, 32}, VK_FORMAT_D16_UNORM, DrawnD16Depth); @@ -390,6 +402,7 @@ int main() { DepthBitsArrayCase(*device, block, output); DepthBitsBaseSliceCase(*device, block, output); FirstSliceCase(*device, block, output); + PaddedCubeCase(*device, block, output); SmallD16CubeCase(*device, block, output); BaseSliceCase(*device, block, output); RefusalCase(*device, block); From 18522aeabb5a4bbcd7f76e246729558d9e9231e4 Mon Sep 17 00:00:00 2001 From: Tyler Westerberg <tylerwesterberg05@gmail.com> Date: Tue, 13 Oct 2026 01:10:17 -0400 Subject: [PATCH 71/81] fix(agc): wait for queued registration preparations before accepting a submission Since registered shaders are prepared in the background, a draw whose shader was registered just before could wait seconds for its preparation inside the queue worker. Under CPU load Goat Simulator 3's first submissions took ~3.7 s; the title treated the GPU as hung, stopped submitting, and its 10 s garbage collection timeout crashed it on every launch. Driver::Submit now waits for the preparations queued before it, on the submitting thread, before the commands reach the GPU. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../include/Driver/Shaders/ShaderRegistry.hpp | 1 + .../src/Driver/Queues/Submission.cpp | 1 + .../src/Driver/Shaders/ShaderRegistry.cpp | 24 ++++++++++++++--- .../tests/execution/PreparedShaders.cpp | 26 +++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 5 files changed, 50 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index 5ba74600788..9aa51c4f40e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -113,6 +113,7 @@ std::shared_ptr<const ShaderSnapshot> RegisteredProgram(const ShaderRegistry& re void PublishRegisteredShader(std::shared_ptr<ShaderRegistry>& registry, const std::shared_ptr<const ShaderSnapshot>& snapshot); void SubmitRegistrationPreparation(std::function<void()> task, const PreparedShaders* prepared = nullptr); void PrioritizeRegistrationPreparation(PreparedShaders& prepared); +void AwaitRegistrationPreparation(); void SubmitLibraryPrebuild(std::function<void()> task); void CloseRegistrationPreparation(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp index db3f64fa8d4..3d428973f27 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp @@ -298,6 +298,7 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue) { if (APS5_ENABLE_TIMING_LOG) submission.validatedAt = std::chrono::steady_clock::now(); waitForFlipRoom(submission); holdFlipBehindWorker(submission); + AwaitRegistrationPreparation(); if (APS5_ENABLE_TIMING_LOG) submission.roomReadyAt = std::chrono::steady_clock::now(); static const bool trace = std::getenv("APS5_TRACE_GPU") != nullptr; if (trace) std::fprintf(stderr, "[gpu] %.1f submit queue=0x%x dwords=%zu at %p\n", TraceMs(), queue, submission.commands.size(), static_cast<const void*>(descriptor.addr)); 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 9a4a6e63eb7..c0f0f595cc1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -13,6 +13,7 @@ #include <algorithm> #include <atomic> #include <chrono> +#include <set> #include <condition_variable> #include <cstdio> #include <cstddef> @@ -894,8 +895,10 @@ class RegistrationPreparePool { { std::unique_lock lock(mutex); if (threads != 0 && !closed) { - if (prepared != nullptr && prepared->demanded.load()) tasks.push_front({std::move(task), prepared}); - else tasks.push_back({std::move(task), prepared}); + const auto sequence = ++submitted; + if (prepared != nullptr && prepared->demanded.load()) tasks.push_front({std::move(task), prepared, sequence}); + else tasks.push_back({std::move(task), prepared, sequence}); + unfinished.insert(sequence); lock.unlock(); ready.notify_one(); return; @@ -912,6 +915,12 @@ class RegistrationPreparePool { std::rotate(tasks.begin(), found, std::next(found)); } + void AwaitSubmitted() { + std::unique_lock lock(mutex); + const auto ticket = submitted; + idle.wait(lock, [&] { return unfinished.empty() || *unfinished.begin() > ticket; }); + } + void Close() { std::unique_lock lock(mutex); closed = true; @@ -945,14 +954,16 @@ class RegistrationPreparePool { pool.tasks.pop_front(); ++pool.running; } + const auto sequence = task.sequence; task.function(); task = {}; bool drained = false; { std::lock_guard lock(pool.mutex); --pool.running; + pool.unfinished.erase(sequence); drained = pool.tasks.empty() && pool.running == 0; - if (drained) pool.idle.notify_all(); + pool.idle.notify_all(); } #ifdef __GLIBC__ if (drained) trimFreedMemory(); @@ -964,6 +975,7 @@ class RegistrationPreparePool { struct Task { std::function<void()> function; const PreparedShaders* prepared = nullptr; + std::uint64_t sequence = 0; }; std::mutex mutex; @@ -972,6 +984,8 @@ class RegistrationPreparePool { std::deque<Task> tasks; unsigned threads = 0; unsigned running = 0; + std::uint64_t submitted = 0; + std::set<std::uint64_t> unfinished; bool closed = false; }; @@ -1017,6 +1031,10 @@ void PrioritizeRegistrationPreparation(PreparedShaders& prepared) { if (auto* pool = RegistrationPreparePool::Existing()) pool->Prioritize(prepared); } +void AwaitRegistrationPreparation() { + if (auto* pool = RegistrationPreparePool::Existing()) pool->AwaitSubmitted(); +} + void SubmitLibraryPrebuild(std::function<void()> task) { RegistrationPreparePool::Prebuild().Submit(std::move(task)); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index c4fb6eba1f0..777dd733f47 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -304,6 +304,31 @@ std::exception_ptr InjectedFailure() { } } +void AwaitedRegistrationPreparation() { + std::atomic<bool> ran{false}; + AgcDriver::DriverDetail::SubmitRegistrationPreparation([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + ran.store(true, std::memory_order_release); + }); + AgcDriver::DriverDetail::AwaitRegistrationPreparation(); + Require(ran.load(std::memory_order_acquire), "a submission did not wait for the registration preparation queued before it"); + std::atomic<bool> slow{false}; + std::atomic<bool> fast{false}; + AgcDriver::DriverDetail::SubmitRegistrationPreparation([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + slow.store(true, std::memory_order_release); + }); + auto waiter = std::async(std::launch::async, [&] { + AgcDriver::DriverDetail::AwaitRegistrationPreparation(); + return slow.load(std::memory_order_acquire); + }); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + AgcDriver::DriverDetail::SubmitRegistrationPreparation([&] { fast.store(true, std::memory_order_release); }); + Require(waiter.get(), "a submission stopped waiting for an earlier preparation once a later one finished"); + AgcDriver::DriverDetail::AwaitRegistrationPreparation(); + Require(fast.load(std::memory_order_acquire), "the later preparation did not run"); +} + void PendingRegistrationPreparation(AgcDriver::VulkanDevice& device) { alignas(256) const std::array<std::uint32_t, 1> code{0xbf810000u}; std::array<std::uint32_t, 4> users{}; @@ -673,6 +698,7 @@ int main(int argc, char** argv) { RegisteredVertexWorkgroup(device->Target()); FailureCapture(*device); PendingRegistrationPreparation(*device); + AwaitedRegistrationPreparation(); Require(argc != 2 || std::string_view(argv[1]) != "--fail-before-registration", "injected failure before registration"); PrepareMultisampledStorage(*device); device.reset(); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 979a7bfa7e7..34988067306 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -132,3 +132,4 @@ - [16-bit depth with stencil](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is stored as `D24_UNORM_S8_UINT`, or `D32_SFLOAT_S8_UINT`, on a device without `D16_UNORM_S8_UINT` as an attachment. Depth comparisons, clears and the depth image see the wider format's precision, so results at the 16-bit quantization boundary can differ from the console; the constant depth bias is scaled by 256 for D24, and a non-zero constant on the D32 float fallback throws. Not measured on console hardware. - [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; a title that waits only on release interrupts sees one extra event per flip, with `data` counting it. - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less 2 GiB (imports and type-3 mirrors share it; #167's rule). A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A refused import is tried again at its next use. The 64 MiB window, the 256-lookup protection and the 2 GiB reserve are choices, not measured. +- [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. From 8fec47a4b2daf2f5de7447fc42ade7bb93efe5e2 Mon Sep 17 00:00:00 2001 From: XairRrR <dev910382@gmail.com> Date: Tue, 13 Oct 2026 14:00:00 +0200 Subject: [PATCH 72/81] fix(agc): give each slice of a depth array its own HTILE A slice of a depth array target (DB_DEPTH_VIEW SLICE_START) offset its depth and stencil addresses but kept the array's DB_HTILE_DATA_BASE as its HTILE. Every slice then shared slice 0's metadata: a fast clear of slice 0's HTILE cleared every slice, and a clear of any other slice's HTILE matched none. A slice's HTILE now starts at the base plus the slice times one slice's pipe-aligned metadata, as addrlib addresses it (sliceSize * slice in Gfx10Lib::HwlComputeHtileAddrFromCoord; HtileMetadataBytes is that slice size). The recognized HTILE clear kernel now reports the bytes it clears, 16 per thread (global_store_dwordx4 at the thread index << 4), and a surface takes the clear when that range covers its HTILE, as a DMA_DATA fill does; a clear over a whole array still clears every slice. Uncharted: Legacy of Thieves Collection (PPSA05684) renders its sun shadow cascades into the four slices of a 2048x2048 D16 array; each slice's HTILE lies 256 KiB after the previous one. --- .../src/Driver/Dispatch/Dispatch.cpp | 5 +++- .../Graphics/include/DepthSurface.hpp | 2 +- .../Graphics/src/DepthSurface.cpp | 21 +++++++++---- .../libSceAgcDriver/Graphics/src/State.cpp | 2 +- .../tests/DepthSurfaceReuse.cpp | 30 ++++++++++++++++++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 7 +++++ 6 files changed, 57 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp index 374129a92bf..9a23e42c8a3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -75,7 +75,10 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet, if (!partialLastGroup && matchesFillKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) return; if (!partialLastGroup && indirectArguments == 0 && matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) return; } else { - if (const auto htile = Graphics::HtileDepthClearAddress(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads); htile != 0) Graphics::NoteHtileDepthClear(htile); + if (const auto htile = Graphics::HtileDepthClearAddress(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads); htile != 0) { + const auto threads = (packet[4] & 0x20u) != 0 ? static_cast<std::uint64_t>(packet[1]) : static_cast<std::uint64_t>(packet[1]) * compute.numThreads[0]; + Graphics::NoteHtileDepthClear(htile, static_cast<std::size_t>(threads * 16u)); + } if (indirectArguments == 0 && (packet[4] & 0x20u) == 0) { if (const auto fill = Graphics::MatchMemoryValueFill(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads, {packet[1], packet[2], packet[3]})) Graphics::NoteMemoryValueFill(*fill); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index 052e452a625..1a8f3fa00c4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -22,7 +22,7 @@ void ClearDepthSurfaces(VkDevice device); bool DepthSurfaceAt(std::uint64_t address); bool DepthSurfaceAt(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource); std::uint64_t HtileDepthClearAddress(std::span<const std::uint32_t> code, std::span<const std::uint32_t> userData, const std::array<std::uint32_t, 3>& numThreads); -void NoteHtileDepthClear(std::uint64_t htileAddress); +void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes); struct MemoryValueFill { std::uint64_t address; std::size_t bytes; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index ce18ed43846..8129c72296f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -617,11 +617,20 @@ DepthSurface* planeSurface(VkDevice device, std::uint64_t address) { return found == list.rend() ? nullptr : found->get(); } -std::set<std::uint64_t>& clearedHtiles() { - static auto* cleared = new std::set<std::uint64_t>(); +std::map<std::uint64_t, std::size_t>& clearedHtiles() { + static auto* cleared = new std::map<std::uint64_t, std::size_t>(); return *cleared; } +bool takeClearedHtile(const DepthTarget& target) { + if (target.htileAddress == 0) return false; + auto& cleared = clearedHtiles(); + const auto found = std::find_if(cleared.begin(), cleared.end(), [&](const auto& entry) { return HtileFillCovers(target.htileAddress, target.extent, entry.first, entry.second); }); + if (found == cleared.end()) return false; + cleared.erase(found); + return true; +} + const DepthSurface* drawnDepth = nullptr; std::atomic<std::uint64_t> surfacesSerial{0}; @@ -678,7 +687,7 @@ float HostDepthBiasConstant(const Context& context, VkFormat format, float const VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) { std::lock_guard lock(surfacesMutex()); - const bool cleared = target.htileAddress != 0 && clearedHtiles().erase(target.htileAddress) != 0; + const bool cleared = takeClearedHtile(target); DepthSurface* surface = nullptr; for (const auto& candidate : surfaces()) { if (candidate->context.device == context.device && sameSurface(candidate->target, target)) { @@ -736,15 +745,15 @@ void NoteMemoryValueFill(const MemoryValueFill& fill) { NoteDepthMetadataFill(fill.address, fill.bytes, value); } -void NoteHtileDepthClear(std::uint64_t htileAddress) { +void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes) { std::lock_guard lock(surfacesMutex()); bool existing = false; for (const auto& surface : surfaces()) { - if (surface->target.htileAddress != htileAddress) continue; + if (!HtileFillCovers(surface->target.htileAddress, surface->target.extent, htileAddress, bytes)) continue; surface->pendingClear |= VK_IMAGE_ASPECT_DEPTH_BIT; existing = true; } - if (!existing) clearedHtiles().insert(htileAddress); + if (!existing) clearedHtiles()[htileAddress] = bytes; } void ClearDepthSurfaces(VkDevice device) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 973a5a6fe6a..6714dc11247 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -292,7 +292,7 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx depth.clearDepth = readFloat(cx, 0x00b); depth.clearStencil = static_cast<std::uint8_t>(read(cx, 0x00a) & 0xffu); if (const auto htile = find(cx, 0x005); zFormat != 0 && htile != cx.end() && htile->second != 0 && (read(cx, 0x010) & (1u << 29u)) != 0) { - depth.htileAddress = base(0x005, 0x01e); + depth.htileAddress = base(0x005, 0x01e) + static_cast<std::uint64_t>(view & 0x1fffu) * HtileMetadataBytes(depth.extent); depth.htileStencil = stencil && (read(cx, 0x011) & (1u << 29u)) == 0; } result.depth = depth; diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp index deec634f74a..066f2eff6f5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp @@ -39,11 +39,14 @@ THandle newHandle() { VkFormat lastImageFormat = VK_FORMAT_UNDEFINED; std::vector<VkImage> createdImages; std::vector<std::tuple<VkImage, VkImage, VkImageAspectFlags>> imageCopies; +VkImage lastImage = VK_NULL_HANDLE; +std::vector<VkImage> clearedImages; VKAPI_ATTR VkResult VKAPI_CALL createImage(VkDevice, const VkImageCreateInfo* info, const VkAllocationCallbacks*, VkImage* image) { lastImageFormat = info->format; *image = newHandle<VkImage>(); createdImages.push_back(*image); + lastImage = *image; return VK_SUCCESS; } @@ -101,7 +104,9 @@ VKAPI_ATTR void VKAPI_CALL destroyImageView(VkDevice, VkImageView, const VkAlloc VKAPI_ATTR void VKAPI_CALL destroyImage(VkDevice, VkImage, const VkAllocationCallbacks*) {} VKAPI_ATTR void VKAPI_CALL freeMemory(VkDevice, VkDeviceMemory, const VkAllocationCallbacks*) {} VKAPI_ATTR void VKAPI_CALL cmdPipelineBarrier(VkCommandBuffer, VkPipelineStageFlags, VkPipelineStageFlags, VkDependencyFlags, std::uint32_t, const VkMemoryBarrier*, std::uint32_t, const VkBufferMemoryBarrier*, std::uint32_t, const VkImageMemoryBarrier*) {} -VKAPI_ATTR void VKAPI_CALL cmdClearDepthStencilImage(VkCommandBuffer, VkImage, VkImageLayout, const VkClearDepthStencilValue*, std::uint32_t, const VkImageSubresourceRange*) {} +VKAPI_ATTR void VKAPI_CALL cmdClearDepthStencilImage(VkCommandBuffer, VkImage image, VkImageLayout, const VkClearDepthStencilValue*, std::uint32_t, const VkImageSubresourceRange*) { + clearedImages.push_back(image); +} VKAPI_ATTR void VKAPI_CALL cmdCopyImage(VkCommandBuffer, VkImage source, VkImageLayout, VkImage destination, VkImageLayout, std::uint32_t count, const VkImageCopy* regions) { for (std::uint32_t i = 0; i < count; ++i) imageCopies.emplace_back(source, destination, regions[i].dstSubresource.aspectMask); } @@ -307,6 +312,29 @@ void RunDepthSurfaceReuseTests() { imageCopies.clear(); const auto otherWithA = pairing(otherDepth, stencilA); Require(imageCopies.size() == 1 && imageCopies[0] == std::make_tuple(withA, otherWithA, static_cast<VkImageAspectFlags>(VK_IMAGE_ASPECT_STENCIL_BIT)), "a stencil plane paired with another depth plane must carry its stencil over"); + { + constexpr std::uint64_t arrayBase = 0x80000000; + constexpr std::uint64_t htile = 0x81000000; + constexpr VkExtent2D extent{1024, 512}; + const auto sliceHtile = HtileMetadataBytes(extent); + DepthTarget first{arrayBase, 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + first.htileAddress = htile; + DepthTarget second{arrayBase + DepthSliceBytes(extent, 4u), 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + second.htileAddress = htile + sliceHtile; + DepthSurfaceView(context, first); + const auto firstImage = lastImage; + DepthSurfaceView(context, second); + const auto secondImage = lastImage; + const auto clearsAfter = [&](std::size_t bytes) { + clearedImages.clear(); + NoteHtileDepthClear(htile, static_cast<std::size_t>(bytes)); + DepthSurfaceView(context, first); + DepthSurfaceView(context, second); + return clearedImages; + }; + Require(clearsAfter(sliceHtile) == std::vector<VkImage>{firstImage}, "an HTILE clear of the first slice must clear that slice alone"); + Require(clearsAfter(2u * sliceHtile) == std::vector<VkImage>{firstImage, secondImage}, "an HTILE clear over both slices must clear both"); + } ClearDepthSurfaces(context.device); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index edb679bc598..c419cb023e9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1484,6 +1484,13 @@ void DepthStencilTests() { AgcDriver::Graphics::RegisterReadLog() = nullptr; for (const auto read : log) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks an HTILE register the decoder reads: " + std::to_string(read.offset)); Require(state.depth->htileAddress == (0x30000ull | (1ull << 40u)), "the HTILE address was not decoded from DB_HTILE_DATA_BASE and its high byte"); + { + const auto view = queue.context[0x002]; + queue.context[0x002] = 2u | (2u << 13u); + const auto slice = AgcDriver::Graphics::DecodeState(queue); + Require(slice.depth->htileAddress == (0x30000ull | (1ull << 40u)) + 2u * AgcDriver::Graphics::HtileMetadataBytes(slice.depth->extent), "slice 2 of a depth array must use the third slice of its HTILE"); + queue.context[0x002] = view; + } queue.context[0x010] &= ~0x20000000u; state = AgcDriver::Graphics::DecodeState(queue); Require(state.depth->htileAddress == 0, "a surface without TILE_SURFACE_ENABLE decoded an HTILE address"); From a84afe931c2d5cd3a92ed7407f56f814dfdf4a7d Mon Sep 17 00:00:00 2001 From: XairRrR <dev910382@gmail.com> Date: Tue, 13 Oct 2026 14:03:42 +0200 Subject: [PATCH 73/81] fix(agc): store a written storage image over a depth plane into the surface DepthSurfaceStoreBack took only the storage image a depth surface had handed out for its plane. A storage image over the same plane that the surface did not hand out (one cached before the surface existed, kept by a dispatch whose resources revalidate from their records) was written and never reached the depth image: later depth tests and plane reads used the surface's old contents. A written storage image that covers a depth surface's depth or stencil plane exactly (the plane's address and extent, level 0, one 2D layer, the plane's 32-bit float, 16-bit unorm or 8-bit uint format) is now copied into that plane when it is stored back, as the surface's own view is. A view the surface's own view replaced still throws. Uncharted: Legacy of Thieves Collection (PPSA05684) writes a 640x360 downsampled depth with a compute shader and then tests its mist particles against it as a depth target; the depth target stayed at its clear and the mist drew over foreground geometry. --- .../Graphics/src/DepthSurface.cpp | 30 +++++++++++++++---- .../tests/execution/DepthStorage.cpp | 11 +++++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index ce18ed43846..2d6013b2b8c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -213,7 +213,7 @@ class DepthSurface { if (plane.storage != nullptr) plane.replaced.push_back(plane.storage); plane.storage = StorageTexture::Create(context, *context.detiler, resource, 0); } - copyPlane(plane, stencil, true); + copyPlane(plane, plane.storage, stencil, true); return plane.storage; } @@ -221,7 +221,7 @@ class DepthSurface { for (const bool stencil : {false, true}) { auto& plane = stencil ? stencilPlane : depthPlane; if (plane.storage.get() == &candidate) { - copyPlane(plane, stencil, false); + copyPlane(plane, plane.storage, stencil, false); written = ++depthWrites; return true; } @@ -230,6 +230,14 @@ class DepthSurface { std::snprintf(text, sizeof(text), "AGC graphics: storage image writes to the %s plane of depth surface 0x%llx through a view that another view of the plane (guest format %u, tile mode %d) replaced are not implemented", stencil ? "stencil" : "depth", static_cast<unsigned long long>(target.address), plane.storage->Descriptor().format, static_cast<int>(plane.storage->Descriptor().tileMode)); throw std::runtime_error(text); } + for (const bool stencil : {false, true}) { + if (!CoversPlane(candidate, stencil)) continue; + auto& plane = stencil ? stencilPlane : depthPlane; + const auto source = std::const_pointer_cast<StorageTexture>(candidate.shared_from_this()); + copyPlane(plane, source, stencil, false); + written = ++depthWrites; + return true; + } return false; } @@ -250,20 +258,30 @@ class DepthSurface { PlaneStorage depthPlane; PlaneStorage stencilPlane; - void copyPlane(PlaneStorage& plane, bool stencil, bool toStorage) { + bool CoversPlane(const StorageTexture& storage, bool stencil) const { + const auto& resource = storage.Descriptor(); + const auto address = stencil ? target.stencilAddress : target.address; + if (address == 0 || resource.baseAddress != address || resource.width != target.extent.width || resource.height != target.extent.height || resource.baseLevel != 0 || resource.mipCount != 1) return false; + if (resource.dimension != TextureDimension::k2D && (resource.dimension != TextureDimension::k2DArray || resource.baseArray != 0 || resource.depthOrLastArray != 0)) return false; + if (target.samples != 1 || (!stencil && hostFormat != target.format)) return false; + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + return ResolveTextureFormat(resource.format) == (stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT); + } + + void copyPlane(PlaneStorage& plane, const std::shared_ptr<StorageTexture>& storage, bool stencil, bool toStorage) { auto* recorder = Recorder::Active(); Require(recorder != nullptr, "depth surface storage copies need an active recorder"); const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; const auto bytes = static_cast<std::size_t>(target.extent.width) * target.extent.height * (stencil ? 1u : d16 ? 2u : 4u); if (plane.staging == nullptr) plane.staging = std::make_unique<DeviceBuffer>(context, bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); const auto commands = recorder->Commands(); - recorder->Keep(plane.storage); + recorder->Keep(storage); const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::Copy); VkBufferImageCopy planeRegion{0, 0, 0, {stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1}, {0, 0, 0}, {target.extent.width, target.extent.height, 1}}; VkBufferImageCopy colorRegion = planeRegion; colorRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - const auto sourceImage = toStorage ? image : plane.storage->Image(); - const auto destinationImage = toStorage ? plane.storage->Image() : image; + const auto sourceImage = toStorage ? image : storage->Image(); + const auto destinationImage = toStorage ? storage->Image() : image; RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(commands, sourceImage, VK_IMAGE_LAYOUT_GENERAL, plane.staging->Handle(), 1, toStorage ? &planeRegion : &colorRegion); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp index 0739bd6b861..521dce3be5a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp @@ -246,6 +246,17 @@ void Run(Context context, std::uint8_t* block) { ExpectFailure([&] { static_cast<void>(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "with a layer pitch of", "a two-layer 2D array view of the depth plane with a layer pitch other than the depth slice size was accepted"); layeredDepth.baseArray = 1; ExpectFailure([&] { static_cast<void>(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "with a layer pitch of", "a 2D array view of layer 1 of the depth plane with a layer pitch other than the depth slice size was accepted"); + const auto own = StorageTexture::Create(context, *context.detiler, PlaneResource(address, Format32Float), 0); + const std::array<std::uint32_t, 4> eighth{std::bit_cast<std::uint32_t>(0.125f), std::bit_cast<std::uint32_t>(0.125f), std::bit_cast<std::uint32_t>(0.125f), std::bit_cast<std::uint32_t>(0.125f)}; + Require(own->FillClear(eighth, StorageTexture::WholeImage, refusal), refusal != nullptr ? refusal : "the storage image could not be filled"); + Require(DepthSurfaceStoreBack(context, *own), "a storage image over the depth plane that the depth surface did not hand out was not stored back"); + submit(recorder); + const auto reread = DepthSurfaceStorage(context, {}, PlaneResource(address, Format32Float), 0); + Require(reread != nullptr, "the depth plane has no storage image"); + submit(recorder); + readBack(*reread); + Require(CountFloats(block, 0.125f) == Side * Side, "a storage image written over the depth plane did not reach the depth surface"); + Require(!DepthSurfaceStoreBack(context, *StorageTexture::Create(context, *context.detiler, PlaneResource(address, Format32Float, Side / 2), 0)), "a storage image of another extent over the depth memory was stored into the depth surface"); } } From b3e5acc8cc48abf73943e95b0780c2d78312f95f Mon Sep 17 00:00:00 2001 From: XairRrR <dev910382@gmail.com> Date: Tue, 13 Oct 2026 18:50:44 +0200 Subject: [PATCH 74/81] fix(agc): store a covering storage image into the newest pairing of its plane DepthSurfaceStoreBack walked the surfaces oldest first and let the first one whose plane the storage image covers take the copy. A depth plane bound with several stencil planes has one surface per pairing, so the copy could land in an older pairing while DepthSurfaceStorage, DepthSurfaceTexture and DepthSurfaceAt read the newest one, and a store back through a replaced view was accepted by an older pairing instead of throwing. The surface's own view and the replaced views are now checked on every surface first; only then does the newest surface covering the plane take the copy, the order DepthSurfaceStorage resolves a plane in. Reported with the repro and the fix by nikowolf123; the repro is the new RunPairings test. AI-assisted: yes --- .../Graphics/src/DepthSurface.cpp | 7 ++++ .../tests/execution/DepthStorage.cpp | 37 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 2d6013b2b8c..b9ce14abb56 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -230,6 +230,10 @@ class DepthSurface { std::snprintf(text, sizeof(text), "AGC graphics: storage image writes to the %s plane of depth surface 0x%llx through a view that another view of the plane (guest format %u, tile mode %d) replaced are not implemented", stencil ? "stencil" : "depth", static_cast<unsigned long long>(target.address), plane.storage->Descriptor().format, static_cast<int>(plane.storage->Descriptor().tileMode)); throw std::runtime_error(text); } + return false; + } + + bool StoreBackCovering(const StorageTexture& candidate) { for (const bool stencil : {false, true}) { if (!CoversPlane(candidate, stencil)) continue; auto& plane = stencil ? stencilPlane : depthPlane; @@ -935,6 +939,9 @@ bool DepthSurfaceStoreBack(const Context& context, const StorageTexture& storage for (const auto& surface : surfaces()) { if (surface->context.device == context.device && surface->StoreBack(storage)) return true; } + for (auto surface = surfaces().rbegin(); surface != surfaces().rend(); ++surface) { + if ((*surface)->context.device == context.device && (*surface)->StoreBackCovering(storage)) return true; + } return false; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp index 521dce3be5a..f83e70edb50 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp @@ -259,6 +259,41 @@ void Run(Context context, std::uint8_t* block) { Require(!DepthSurfaceStoreBack(context, *StorageTexture::Create(context, *context.detiler, PlaneResource(address, Format32Float, Side / 2), 0)), "a storage image of another extent over the depth memory was stored into the depth surface"); } +void RunPairings(Context context, std::uint8_t* block) { + const auto address = reinterpret_cast<std::uint64_t>(block); + const auto submit = [&](Recorder& recorder) { + recorder.Submit(); + Check(context.Function<PFN_vkQueueWaitIdle>("vkQueueWaitIdle")(context.queue), "vkQueueWaitIdle"); + }; + Recorder recorder(context); + recorder.Activate(); + const auto readBack = [&](StorageTexture& texture) { + texture.MarkDirty(); + texture.WriteBack(); + submit(recorder); + }; + const DepthTarget first{address, address + BlockBytes / 2, {Side, Side}, VK_FORMAT_D32_SFLOAT_S8_UINT, 0.25f, 7}; + const DepthTarget second{address, address + BlockBytes / 2 + 8192, {Side, Side}, VK_FORMAT_D32_SFLOAT_S8_UINT, 0.5f, 3}; + Require(DepthSurfaceView(context, first) != VK_NULL_HANDLE && DepthSurfaceView(context, second) != VK_NULL_HANDLE, "the two pairings of the depth plane were not created"); + const auto depth = DepthSurfaceStorage(context, {}, PlaneResource(address, Format32Float), 0); + Require(depth != nullptr, "the depth plane has no storage image"); + submit(recorder); + const std::array<std::uint32_t, 4> pattern{std::bit_cast<std::uint32_t>(0.75f), std::bit_cast<std::uint32_t>(0.75f), std::bit_cast<std::uint32_t>(0.75f), std::bit_cast<std::uint32_t>(0.75f)}; + const char* refusal = nullptr; + Require(depth->FillClear(pattern, StorageTexture::WholeImage, refusal), refusal != nullptr ? refusal : "the storage image could not be filled"); + Require(DepthSurfaceStoreBack(context, *depth), "the newest pairing's storage image was not stored back"); + submit(recorder); + const auto reread = DepthSurfaceStorage(context, {}, PlaneResource(address, Format32Float), 0); + Require(reread != nullptr, "the depth plane has no storage image"); + submit(recorder); + readBack(*reread); + Require(CountFloats(block, 0.75f) == Side * Side, "a storage image stored back went into an older pairing instead of the pairing that handed it out"); + auto layered = PlaneResource(address, Format32Float); + layered.dimension = TextureDimension::k2DArray; + Require(DepthSurfaceStorage(context, {}, layered, 0) != nullptr, "a one-layer 2D array view of the depth plane was not taken"); + ExpectFailure([&] { static_cast<void>(DepthSurfaceStoreBack(context, *depth)); }, "through a view that another view of the plane", "a store back through a replaced view was taken by an older pairing that covers the plane"); +} + } int main() { @@ -280,6 +315,8 @@ int main() { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); Run(context, block); ClearDepthSurfaces(context.device); + RunPairings(context, block); + ClearDepthSurfaces(context.device); } GuestAllocations::Mutation().Remove(block); std::cout << "depth surface storage access tests passed\n"; From 9394a6f51adb21b156a3938beb19a05983c99669 Mon Sep 17 00:00:00 2001 From: Swastik-51 <Swastik-51@users.noreply.github.com> Date: Tue, 13 Oct 2026 02:41:42 +0530 Subject: [PATCH 75/81] fix(agc): handle FMASK decompress on MSAA targets --- .../Graphics/include/MultisampleTarget.hpp | 1 + .../Graphics/include/State.hpp | 2 +- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 6 ++ .../Graphics/src/MultisampleTarget.cpp | 5 + .../libSceAgcDriver/Graphics/src/State.cpp | 13 ++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 26 +++++ .../prx/libSceAgcDriver/tests/Recorder.cpp | 100 ++++++++++++++++++ 7 files changed, 148 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index 97980329038..62832614458 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -53,6 +53,7 @@ bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, V bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colorFill, const ColorTarget& use, VkClearColorValue& clear); VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target); std::shared_ptr<Texture> MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components); +void MaterializeMultisampleTargetClear(const Context& context, const ColorTarget& target); void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView); bool MultisampledColorFillAbsorbs(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index df145d6d236..a27fb8bb9c5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -147,7 +147,7 @@ std::uint32_t ColorWriteMask(const Registers& context); GuestTextureResource SurfaceForTarget(const ColorTarget& color); struct ColorMetadataPass { - enum class Mode { EliminateFastClear, DccDecompress, Resolve }; + enum class Mode { EliminateFastClear, DccDecompress, Resolve, FmaskDecompress }; Mode mode; std::vector<ColorTarget> targets; std::optional<ColorTarget> source; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 9ea76c5ac64..af1821fc173 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2573,6 +2573,12 @@ void RunColorMetadataPass(const Context& context, const ColorMetadataPass& pass) ResolveMultisampleTarget(context, *pass.source, *resident, resident->AttachmentView(destination.format, destination.mip)); return; } + if (pass.mode == ColorMetadataPass::Mode::FmaskDecompress) { + if (context.device != VK_NULL_HANDLE) { + for (const auto& color : pass.targets) MaterializeMultisampleTargetClear(context, color); + } + return; + } for (const auto& color : pass.targets) { if (color.cmaskAddress != 0) { const auto resident = metadataPassResident(context, color); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index be3c43dfbdb..87b9158eb2a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -422,6 +422,11 @@ std::shared_ptr<Texture> MultisampleTargetTexture(const Context& context, const throw std::runtime_error(text); } +void MaterializeMultisampleTargetClear(const Context& context, const ColorTarget& target) { + std::lock_guard lock(targetsMutex()); + acquire(context, target); +} + void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView) { std::lock_guard lock(targetsMutex()); auto& target = acquire(context, source); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 973a5a6fe6a..5f50608915f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1123,7 +1123,6 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { Require(color.samples <= 8u, "color targets with more than eight samples are unsupported"); Require((1u << ((attrib >> 15u) & 3u)) == color.samples, "color targets with fewer fragments than samples (EQAA) are unsupported"); const auto multisampleInfo = color.samples > 1 ? 0x0c006000u : 0u; - Require(color.samples == 1 || (info & 0x10000000u) == 0, "DCC over a multisampled color target is unsupported"); // ROUND_MODE (bit 18) only affects unorm rounding. With DCC_ENABLE (bit 28) the target is written if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u | 0x00002000u | multisampleInfo)) != 0) throw std::runtime_error("AGC graphics: color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported (CB_COLOR_INFO 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", info); return std::string(text); }() + ")"); Require((info & 0x8000u) != 0 || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported"); @@ -1248,8 +1247,9 @@ std::optional<ColorMetadataPass> DecodeColorMetadataPass(const QueueState& queue const auto sourceAttrib = find(cx, 0x31d); const bool sourceSamples = sourceAttrib != cx.end() && ((sourceAttrib->second >> 12u) & 7u) != 0; const bool resolve = mode == 3u && (rasterizerSamples || sourceSamples); - if (mode != 2u && mode != 6u && !resolve) return std::nullopt; - ColorMetadataPass pass{resolve ? ColorMetadataPass::Mode::Resolve : mode == 2u ? ColorMetadataPass::Mode::EliminateFastClear : ColorMetadataPass::Mode::DccDecompress, {}, std::nullopt}; + const bool fmaskDecompress = mode == 5u; + if (mode != 2u && mode != 6u && !resolve && !fmaskDecompress) return std::nullopt; + ColorMetadataPass pass{resolve ? ColorMetadataPass::Mode::Resolve : fmaskDecompress ? ColorMetadataPass::Mode::FmaskDecompress : mode == 2u ? ColorMetadataPass::Mode::EliminateFastClear : ColorMetadataPass::Mode::DccDecompress, {}, std::nullopt}; Require((control->second & ~0x70u) == 0xcc0000u, "CB metadata pass with a nonstandard ROP, dual quads disabled or degamma"); const auto value = [&](std::uint32_t offset) { const auto found = find(cx, offset); @@ -1300,7 +1300,12 @@ std::optional<ColorMetadataPass> DecodeColorMetadataPass(const QueueState& queue for (std::uint32_t slot = 0; slot < 8; ++slot) { if (((targetMask >> (4u * slot)) & 0xfu) == 0 || ((read(cx, 0x31c + slot * 0xfu) >> 2u) & 0x1fu) == 0) continue; const auto target = DecodeColorBuffer(cx, slot); - Require(target.samples == 1, "CB metadata pass over a multisampled color target, whose texels are on the host, is unsupported (only a CB resolve is modeled)"); + if (fmaskDecompress) { + Require(target.samples > 1, "CB metadata pass over a single-sample target"); + Require(target.cmaskAddress != 0, "FMASK decompress of a target without a CMASK address"); + } else { + Require(target.samples == 1, "CB metadata pass over a multisampled color target, whose texels are on the host, is unsupported (only a CB resolve is modeled)"); + } Require((read(cx, 0x31c + slot * 0xfu) & 0x10000000u) == 0 || target.dccAddress != 0, "CB metadata pass over a mipmapped DCC color target, whose keys are not modeled"); requireCovered(target); pass.targets.push_back(target); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index edb679bc598..289652b042e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2520,6 +2520,31 @@ void multisampleTests() { queue.context[0x0] = 0; queue.context[0x202] = 0xcc0020; expectFailure([&] { DecodeColorMetadataPass(queue); }, "multisampled color target"); + queue = multisampled(); + queue.context[0x2f8] = 0; + queue.context[0x0] = 0; + queue.context[0x202] = 0x00cc0050; + queue.context[0x31c] = 0x1002e028; + queue.context[0x31d] = 0x00012000; + queue.context[0x325] = 0x2000; + queue.context[0x323] = 0x3f800000; + queue.context[0x324] = 0x3f800000; + setCmaskAddress(queue); + auto fmaskPass = DecodeColorMetadataPass(queue); + Require(fmaskPass.has_value() && fmaskPass->mode == ColorMetadataPass::Mode::FmaskDecompress && fmaskPass->targets.size() == 1 && fmaskPass->targets[0].samples == 4 && fmaskPass->targets[0].cmaskAddress == cmaskAddress && fmaskPass->targets[0].dccAddress != 0, "an FMASK decompress of a 4x multisampled target with CMASK and DCC did not decode"); + Require(fmaskPass->targets[0].clearWords[0] == 0x3f800000 && fmaskPass->targets[0].clearWords[1] == 0x3f800000, "fast clear words changed"); + AgcDriver::Graphics::RunColorMetadataPass({}, *fmaskPass); + queue = makeState(); + queue.context[0x0] = 0; + queue.context[0x202] = 0x00cc0050; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "over a single-sample target"); + queue = multisampled(); + queue.context[0x2f8] = 0; + queue.context[0x0] = 0; + queue.context[0x202] = 0x00cc0050; + queue.context[0x31c] = 0x1002e028; + queue.context[0x31d] = 0x00012000; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "without a CMASK address"); using AgcDriver::Graphics::DccKeys; using AgcDriver::Graphics::MultisampledCmask; @@ -2539,6 +2564,7 @@ void multisampleTests() { MultisampledCmask seeded(0x30000, 0x1000); seeded.SeedKeys(DccKeys::Clear0000); Require(seeded.TakeClear(), "a fast-cleared seed did not clear its first use"); + Require(!seeded.TakeClear(), "a subsequent use applied the clear again"); MultisampledCmask mixedSeed(0x30000, 0x1000); mixedSeed.SeedKeys(DccKeys::Mixed); expectFailure([&] { mixedSeed.TakeClear(); }, "not modeled"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 1706883d5ad..450ee2506d9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5963,6 +5963,104 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { std::cout << "Multisampled target guest access tests passed\n"; } +void fmaskDecompressMsaaTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0 || base.hostImportAlignment == 0) { + std::cout << "4x color targets or host imports unavailable: FMASK decompress not tested\n"; + return; + } + constexpr std::uint32_t side = 64; + constexpr std::size_t targetBytes = 65536u; + constexpr std::size_t cmaskBytes = 4096u; + constexpr std::size_t destBytes = targetBytes; + constexpr std::size_t bytes = 5u * 65536u + cmaskBytes + 2u * destBytes; +#ifdef _WIN32 + void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + void* block = std::aligned_alloc(65536, bytes); +#endif + Require(block != nullptr, "cannot allocate the FMASK decompress block"); + std::memset(block, 0, bytes); + const auto address = reinterpret_cast<std::uint64_t>(block); + const auto cmaskAddress = address + 5u * 65536u; + const auto destAddress = cmaskAddress + cmaskBytes; + const auto destAddress2 = destAddress + destBytes; + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + ~Unregister() { + ClearMultisampleTargets(context.device); + ClearCachedTextures(context.device); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + } unregister{base, block}; + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + ColorTarget source{}; + source.address = address; + source.surfaceAddress = address; + source.extent = {side, side}; + source.surfaceExtent = source.extent; + source.format = VK_FORMAT_R8G8B8A8_UNORM; + source.elementBytes = 4; + source.bytes = targetBytes; + source.samples = 4; + source.cmaskAddress = cmaskAddress; + source.cmaskBytes = cmaskBytes; + source.cmaskFastClear = true; + source.clearWords = {0x11223344u, 0}; + ColorTarget destination{}; + destination.address = destAddress; + destination.surfaceAddress = destAddress; + destination.extent = {side, side}; + destination.surfaceExtent = destination.extent; + destination.format = VK_FORMAT_R8G8B8A8_UNORM; + destination.elementBytes = 4; + destination.bytes = destBytes; + destination.samples = 1; + destination.tileMode = ColorTileMode::RenderTarget; + { + std::lock_guard gpu(GpuMutex()); + RunColorMetadataPass(context, {ColorMetadataPass::Mode::FmaskDecompress, {source}}); + RunColorMetadataPass(context, {ColorMetadataPass::Mode::Resolve, {destination}, source}); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + std::vector<std::uint32_t> resolved(destBytes / 4); + AgcDriver::GuestMemory::Read(destAddress, std::as_writable_bytes(std::span(resolved))); + Require(std::ranges::all_of(resolved, [](auto word) { return word == 0x11223344u; }), "FMASK decompress did not materialize the seeded clear to the host multisampled image"); + source.clearWords = {0x55667788u, 0}; + ColorTarget destination2 = destination; + destination2.address = destAddress2; + destination2.surfaceAddress = destAddress2; + { + std::lock_guard gpu(GpuMutex()); + RunColorMetadataPass(context, {ColorMetadataPass::Mode::FmaskDecompress, {source}}); + RunColorMetadataPass(context, {ColorMetadataPass::Mode::Resolve, {destination2}, source}); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + std::vector<std::uint32_t> resolved2(destBytes / 4); + AgcDriver::GuestMemory::Read(destAddress2, std::as_writable_bytes(std::span(resolved2))); + Require(std::ranges::all_of(resolved2, [](auto word) { return word == 0x11223344u; }), "subsequent FMASK decompress re-cleared the multisampled target"); + std::cout << "FMASK decompress MSAA clear and resolve tests passed\n"; +} + void depthSurfaceProofTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); if (!device.NullDescriptors()) { @@ -7376,6 +7474,7 @@ int main(int argc, char** argv) { } if (argc == 2 && std::string_view(argv[1]) == "--multisample-only") { multisampleGuestAccessTests(device, recorder); + fmaskDecompressMsaaTests(device, recorder); return 0; } if (argc == 2 && std::string_view(argv[1]) == "--draw-descriptors-only") { @@ -7418,6 +7517,7 @@ int main(int argc, char** argv) { readWrittenStagingTests(device, recorder); depthSurfaceProofTests(device, recorder); multisampleGuestAccessTests(device, recorder); + fmaskDecompressMsaaTests(device, recorder); misalignedSnapshotTests(device, recorder); misalignedRegionTests(device, recorder); drawSnapshotReuseTests(device, recorder); From 8c76693344f8f951bf1302adb5e37e5f3258cfc5 Mon Sep 17 00:00:00 2001 From: Swastik-51 <Swastik-51@users.noreply.github.com> Date: Tue, 13 Oct 2026 04:17:37 +0530 Subject: [PATCH 76/81] test(agc): tighten FMASK decompress tests --- core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp | 4 +--- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 5 ++--- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 6 ++++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index af1821fc173..6b1fd6f8e5d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2574,9 +2574,7 @@ void RunColorMetadataPass(const Context& context, const ColorMetadataPass& pass) return; } if (pass.mode == ColorMetadataPass::Mode::FmaskDecompress) { - if (context.device != VK_NULL_HANDLE) { - for (const auto& color : pass.targets) MaterializeMultisampleTargetClear(context, color); - } + for (const auto& color : pass.targets) MaterializeMultisampleTargetClear(context, color); return; } for (const auto& color : pass.targets) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 289652b042e..4c632cbde1e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2533,7 +2533,6 @@ void multisampleTests() { auto fmaskPass = DecodeColorMetadataPass(queue); Require(fmaskPass.has_value() && fmaskPass->mode == ColorMetadataPass::Mode::FmaskDecompress && fmaskPass->targets.size() == 1 && fmaskPass->targets[0].samples == 4 && fmaskPass->targets[0].cmaskAddress == cmaskAddress && fmaskPass->targets[0].dccAddress != 0, "an FMASK decompress of a 4x multisampled target with CMASK and DCC did not decode"); Require(fmaskPass->targets[0].clearWords[0] == 0x3f800000 && fmaskPass->targets[0].clearWords[1] == 0x3f800000, "fast clear words changed"); - AgcDriver::Graphics::RunColorMetadataPass({}, *fmaskPass); queue = makeState(); queue.context[0x0] = 0; queue.context[0x202] = 0x00cc0050; @@ -2542,9 +2541,9 @@ void multisampleTests() { queue.context[0x2f8] = 0; queue.context[0x0] = 0; queue.context[0x202] = 0x00cc0050; - queue.context[0x31c] = 0x1002e028; + queue.context[0x31c] = 0x00008028; queue.context[0x31d] = 0x00012000; - expectFailure([&] { DecodeColorMetadataPass(queue); }, "without a CMASK address"); + expectFailure([&] { DecodeColorMetadataPass(queue); }, "FMASK decompress of a target without a CMASK address"); using AgcDriver::Graphics::DccKeys; using AgcDriver::Graphics::MultisampledCmask; diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 450ee2506d9..c445f888d9f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5969,7 +5969,7 @@ void fmaskDecompressMsaaTests(const Device& device, Recorder& recorder) { std::cout << "4x color targets or host imports unavailable: FMASK decompress not tested\n"; return; } - constexpr std::uint32_t side = 64; + constexpr std::uint32_t side = 128; constexpr std::size_t targetBytes = 65536u; constexpr std::size_t cmaskBytes = 4096u; constexpr std::size_t destBytes = targetBytes; @@ -6035,7 +6035,9 @@ void fmaskDecompressMsaaTests(const Device& device, Recorder& recorder) { { std::lock_guard gpu(GpuMutex()); RunColorMetadataPass(context, {ColorMetadataPass::Mode::FmaskDecompress, {source}}); - RunColorMetadataPass(context, {ColorMetadataPass::Mode::Resolve, {destination}, source}); + auto recolored = source; + recolored.clearWords = {0x55667788u, 0}; + RunColorMetadataPass(context, {ColorMetadataPass::Mode::Resolve, {destination}, recolored}); recorder.Submit(); device.WaitQueue(); recorder.Sync(); From 02974ea767e2cb2d3fa0817ff42a8bb970d3c61d Mon Sep 17 00:00:00 2001 From: Swastik-51 <Swastik-51@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:14:10 +0530 Subject: [PATCH 77/81] fix(agc): reject DCC on multisampled targets outside FMASK decompress --- .../prx/libSceAgcDriver/Graphics/include/State.hpp | 2 +- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 5 +++-- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 10 ++++++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index a27fb8bb9c5..172ca2f31c0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -141,7 +141,7 @@ VkExtent2D SampleLocationGrid(const State& state); std::array<std::uint8_t, 8> ExportMappings(const State& state); std::array<ShaderRecompiler::ColorExportPacking, 8> ExportPackings(const State& state); ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state); -ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot); +ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot, bool allowMultisampleDcc = false); std::size_t CmaskBytes(std::uint32_t width, std::uint32_t height); std::uint32_t ColorWriteMask(const Registers& context); GuestTextureResource SurfaceForTarget(const ColorTarget& color); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 5f50608915f..e34e0d28095 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1107,7 +1107,7 @@ GuestTextureResource SurfaceForTarget(const ColorTarget& color) { return surface; } -ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { +ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allowMultisampleDcc) { const auto stride = slot * 0xfu; ColorTarget color{}; color.slot = slot; @@ -1123,6 +1123,7 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { Require(color.samples <= 8u, "color targets with more than eight samples are unsupported"); Require((1u << ((attrib >> 15u) & 3u)) == color.samples, "color targets with fewer fragments than samples (EQAA) are unsupported"); const auto multisampleInfo = color.samples > 1 ? 0x0c006000u : 0u; + Require(allowMultisampleDcc || color.samples == 1 || (info & 0x10000000u) == 0, "DCC over a multisampled color target is unsupported"); // ROUND_MODE (bit 18) only affects unorm rounding. With DCC_ENABLE (bit 28) the target is written if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u | 0x00002000u | multisampleInfo)) != 0) throw std::runtime_error("AGC graphics: color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported (CB_COLOR_INFO 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", info); return std::string(text); }() + ")"); Require((info & 0x8000u) != 0 || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported"); @@ -1299,7 +1300,7 @@ std::optional<ColorMetadataPass> DecodeColorMetadataPass(const QueueState& queue const auto targetMask = read(cx, 0x8e); for (std::uint32_t slot = 0; slot < 8; ++slot) { if (((targetMask >> (4u * slot)) & 0xfu) == 0 || ((read(cx, 0x31c + slot * 0xfu) >> 2u) & 0x1fu) == 0) continue; - const auto target = DecodeColorBuffer(cx, slot); + const auto target = DecodeColorBuffer(cx, slot, fmaskDecompress); if (fmaskDecompress) { Require(target.samples > 1, "CB metadata pass over a single-sample target"); Require(target.cmaskAddress != 0, "FMASK decompress of a target without a CMASK address"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 4c632cbde1e..7d5a4392028 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2259,6 +2259,7 @@ VKAPI_ATTR VkResult VKAPI_CALL sampleLocationDepthProperties(VkPhysicalDevice, V void multisampleTests() { using AgcDriver::Graphics::ColorMetadataPass; + using AgcDriver::Graphics::DecodeColorBuffer; using AgcDriver::Graphics::DecodeColorMetadataPass; using AgcDriver::Graphics::DecodeState; const auto multisampled = [] { @@ -2475,6 +2476,10 @@ void multisampleTests() { queue.context[0x31f] = 0; expectFailure([&] { DecodeState(queue); }, "without a CMASK address"); queue = multisampled(); + queue.context[0x31c] |= 0x10000000u; + expectFailure([&] { DecodeState(queue); }, "DCC over a multisampled color target is unsupported"); + expectFailure([&] { DecodeColorBuffer(queue.context, 0); }, "DCC over a multisampled color target is unsupported"); + queue = multisampled(); queue.context[0x31c] |= 0x2000; setCmaskAddress(queue); std::vector<AgcDriver::Graphics::RegisterRead> reads; @@ -2505,6 +2510,9 @@ void multisampleTests() { queue = rasterizedResolve; queue.context[0x32b] = queue.context[0x31c] ^ (1u << 8u); expectFailure([&] { DecodeColorMetadataPass(queue); }, "different formats or extents"); + queue = rasterizedResolve; + queue.context[0x31c] |= 0x10000000u; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "DCC over a multisampled color target is unsupported"); queue = multisampled(); queue.context[0x0] = 0; queue.context[0x202] = 0xcc0030; @@ -2520,6 +2528,8 @@ void multisampleTests() { queue.context[0x0] = 0; queue.context[0x202] = 0xcc0020; expectFailure([&] { DecodeColorMetadataPass(queue); }, "multisampled color target"); + queue.context[0x31c] |= 0x10000000u; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "DCC over a multisampled color target is unsupported"); queue = multisampled(); queue.context[0x2f8] = 0; queue.context[0x0] = 0; From a15f163245a75b9cb3caa0cefeb27702c003b2e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Wed, 14 Oct 2026 19:10:24 +0200 Subject: [PATCH 78/81] docs(debt): FMASK decompress over multisampled targets Co-Authored-By: Claude <noreply@anthropic.com> --- 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 ffa3682cbf2..20b5239912e 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -90,7 +90,7 @@ - [Multisampled CMASK](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size. - [Multisampled CMASK state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target has no guest texels, so its CMASK state comes from fills rather than from a read at each draw as a single-sample target's does. A fill of the whole CMASK with 0 clears the host image at the target's next use, one with 0xffffffff marks it expanded, and any other pattern, or a fill that covers only part of the CMASK, throws at the next use (inferred: main's rule that a mixed CMASK is unmodeled, applied to the one host image). A FAST_CLEAR target starts from its CMASK keys at its first use, and a COMPRESSION-only target starts expanded. Copies by DMA into the CMASK are not seen, and a later use with another CMASK address throws. The clear does not rewrite the CMASK bytes as expanded, so a new target over the same CMASK (a different extent, so a different image) seeds from them again and clears once more. - [Multisampled color fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target's samples are on the host, so a fill of its color memory is applied at its next use: a uniform fill over the whole of it clears the host image to that value, and a pending CMASK fast clear takes precedence over the fill and over a fill that would throw. Its color memory is a single mip and slice of addrlib's block dimensions for its sample count (ComputeThinBlockDimension). A fill over part of it, a fill reaching the start of a target whose footprint is not computed (linear), and a fill value with no exact clear value in the format (sRGB channels other than 0 and 1) throw at the next use. A fill before the target's first use is not seen (the host image starts at 0), nor are copies or shader writes into its color memory. A uniform compute fill over exactly the whole color memory with an exact clear value is not stored into guest memory, since only the host image holds the samples. A target is retired only when its whole guarded range is unmapped or its allocation removed, so if a title reuses that memory for other data while it stays mapped, a read the guard does not check (a buffer read through a V# or a pointer) sees the bytes from before the fill. -- [Multisampled targets in CB passes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve is modeled. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. +- [Multisampled targets in CB passes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve and an FMASK decompress (`CB_COLOR_CONTROL` mode 5) are modeled. The FMASK decompress only applies the pending CMASK fast clear to the host image; it requires a CMASK address, and the target's DCC and FMASK contents are not interpreted. DCC over a multisampled target is accepted only in that pass and throws everywhere else. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. - [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted. - [CB resolve sample count](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with CB_COLOR_CONTROL in resolve mode is a CB resolve when either PA_SC_AA_CONFIG or CB_COLOR0_ATTRIB.NUM_SAMPLES is multisampled, and takes its sample count from CB0. Inferred, not from documentation: PPSA36082 resolves its 4x targets with no sample count in PA_SC_AA_CONFIG, and radeonsi sets PA_SC_AA_CONFIG to 0 for a multisampled framebuffer when rasterizer multisampling and line smoothing are off. - [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid use a 2x2 sample location grid, anchored at the framebuffer origin like the guest's while the window offset is zero, on devices whose `maxSampleLocationGridSize` for the sample count is a multiple of 2x2, and throw on others. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. From 03b7cb5a14da2010e75b4984d1ea760b54137344 Mon Sep 17 00:00:00 2001 From: Nuno Silva <22588915+NFSS10@users.noreply.github.com> Date: Tue, 13 Oct 2026 10:42:54 +0100 Subject: [PATCH 79/81] fix(agc): gather a sampled depth cube at the descriptor's layer offsets A sampled depth texture whose layers are all backed by host depth surfaces is gathered host-to-host, plane by plane. The gather located the surface for layer n at base.address + n * DepthSliceBytes(extent), the host depth slice stride, and required the guest descriptor's sliceLinearBytes to equal that stride. Bendy and the Ink Machine (PPSA27616) samples a 64x64 F32 cube over six D32 depth surfaces. The descriptor's per-layer footprint is the tiled depth slice, but its linear footprint is smaller, so the check refused a valid resource and the game aborted as soon as it sampled the cube. Locate each layer at base.address + geometry.GuestLayerOffset(layer) instead. That is the address the descriptor itself uses, and the one the guest-memory fallback already used, so both ways of finding a layer agree by construction. Keep the geometry check for the mixed case, where a layer has no host surface and is filled from guest memory, because that path lays its layers out at sliceLinearBytes. The storage-image path still refuses a layer pitch that is not the depth slice size; that is unchanged and out of scope. --- .../Graphics/src/DepthSurface.cpp | 6 +- .../tests/execution/DepthPlaneGuestLayers.cpp | 86 +++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index a16d0d64c40..1edcb4ae5f4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -330,7 +330,8 @@ class DepthPlaneCopy { std::shared_ptr<Texture> Refresh(std::span<const DepthPlaneSlice> slices, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType, std::uint32_t baseLayer) { Require(slices.size() == layers, "depth plane copy slices do not match its layers"); const auto geometry = DescribeSurface(resource); - if (geometry.layers != layers || geometry.sliceLinearBytes < sliceBytes() || geometry.mips.empty()) { + const bool guestFill = std::any_of(slices.begin(), slices.end(), [](const auto& slice) { return slice.image == VK_NULL_HANDLE; }); + if (guestFill && (geometry.layers != layers || geometry.sliceLinearBytes < sliceBytes() || geometry.mips.empty())) { char text[320]; std::snprintf(text, sizeof(text), "AGC graphics: depth plane copy geometry does not match its layers (copy %ux%u, %u layers, %llu bytes a slice; texture %ux%u guest format %u, dimension %d, tile mode %u, %u layers, %llu bytes a slice, %zu mips)", extent.width, extent.height, layers, static_cast<unsigned long long>(sliceBytes()), resource.width, resource.height, resource.format, static_cast<int>(resource.dimension), resource.tileMode, geometry.layers, static_cast<unsigned long long>(geometry.sliceLinearBytes), geometry.mips.size()); throw std::runtime_error(text); @@ -807,8 +808,9 @@ std::shared_ptr<Texture> DepthSurfaceTexture(const Context& context, std::span<c throw std::runtime_error(text); } std::vector<DepthPlaneSlice> slices; + const auto geometry = DescribeSurface(resource); for (std::uint32_t layer = 0; layer < layers; ++layer) { - const auto address = base.address + static_cast<std::uint64_t>(layer) * DepthSliceBytes(base.extent, d16 ? 2u : 4u); + const auto address = base.address + geometry.GuestLayerOffset(layer); auto* slice = depthSlice(context.device, address, base); if (slice != nullptr) slice->ApplyFastClear(); if (slice == nullptr) slices.push_back({}); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp index 797f0862870..e411ce0f3fd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp @@ -28,6 +28,7 @@ #include <optional> #include <stdexcept> #include <string> +#include <string_view> #include <vector> namespace { @@ -313,6 +314,89 @@ void RecordedUploads(const Context& context, const WatchedBlock& block) { recorder.Sync(); } +void RunCubeGather(const Context& base, const WatchedBlock& block) { + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.width = Side; + resource.height = Side; + resource.depthOrLastArray = 5; + resource.mipCount = 2; + resource.tileMode = TextureTileMode::kZ64KBX; + resource.dimension = TextureDimension::kCube; + resource.format = Format32Float; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + const auto geometry = DescribeSurface(resource); + Require(geometry.layerBytes != LayerBytes, "the cube fixture's per-layer footprint must not match one host depth slice"); + const auto address = block.Address(); + resource.baseAddress = address; + ClearDepthSurfaces(context.device); + for (std::uint32_t layer = 0; layer < 6; ++layer) { + DepthSurfaceView(context, DepthTarget{address + geometry.GuestLayerOffset(layer), 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}); + } + imageToBufferCopies = 0; + bufferToImageCopies = 0; + const auto texture = DepthSurfaceTexture(context, words, resource, components); + Require(texture != nullptr, "a cube over six depth surfaces at the descriptor's layer offsets was not sampled through their depth planes"); + if (imageToBufferCopies.load() != 6 || bufferToImageCopies.load() != 1) { + throw std::runtime_error("sampling a cube over six depth surfaces recorded " + std::to_string(imageToBufferCopies.load()) + " depth surface copies and " + std::to_string(bufferToImageCopies.load()) + " plane copy fills, expected 6 and 1"); + } + ClearDepthSurfaces(context.device); + const auto refused = [&](const GuestTextureResource& cube) { + try { + DepthSurfaceTexture(context, {}, cube, components); + } catch (const std::runtime_error& error) { + return std::string_view(error.what()).find("depth plane copy geometry does not match its layers") != std::string_view::npos; + } + return false; + }; + for (std::uint32_t layer = 0; layer < 5; ++layer) { + DepthSurfaceView(context, DepthTarget{address + geometry.GuestLayerOffset(layer), 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}); + } + Require(refused(resource), "a cube whose descriptor stride does not match one host depth slice and that is missing a face must be refused"); + ClearDepthSurfaces(context.device); +} + +void RunCubeTiledSlice(const Context& base, const WatchedBlock& block) { + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + constexpr VkExtent2D extent{64, 64}; + GuestTextureResource resource{}; + resource.width = extent.width; + resource.height = extent.height; + resource.depthOrLastArray = 5; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kZ64KBX; + resource.dimension = TextureDimension::kCube; + resource.format = Format32Float; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + const auto geometry = DescribeSurface(resource); + const auto slice = DepthSliceBytes(extent, 4u); + Require(geometry.layerBytes == slice && geometry.sliceLinearBytes != slice, "the tiled cube fixture must have a layer footprint equal to a depth slice but a different linear footprint"); + const auto address = block.Address(); + resource.baseAddress = address; + ClearDepthSurfaces(context.device); + for (std::uint32_t layer = 0; layer < 6; ++layer) { + DepthSurfaceView(context, DepthTarget{address + geometry.GuestLayerOffset(layer), 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}); + } + imageToBufferCopies = 0; + bufferToImageCopies = 0; + const auto texture = DepthSurfaceTexture(context, words, resource, components); + Require(texture != nullptr, "a tiled cube whose linear layer footprint differs from a depth slice was not sampled through its depth planes"); + if (imageToBufferCopies.load() != 6 || bufferToImageCopies.load() != 1) { + throw std::runtime_error("sampling a tiled cube over six depth surfaces recorded " + std::to_string(imageToBufferCopies.load()) + " depth surface copies and " + std::to_string(bufferToImageCopies.load()) + " plane copy fills, expected 6 and 1"); + } + ClearDepthSurfaces(context.device); +} + } int main() { @@ -337,6 +421,8 @@ int main() { Require(AgcDriver::GuestMemory::Watched(block.Address(), BlockBytes), "depth plane guest layers: the guest block is not write-watched"); Run(context, block); RecordedUploads(context, block); + RunCubeGather(context, block); + RunCubeTiledSlice(context, block); } device.reset(); std::puts("depth plane guest layer tests passed"); From ededc2eb653be57b5d3f2235b4fbc2ecc5685a73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Wed, 14 Oct 2026 20:01:37 +0200 Subject: [PATCH 80/81] test(agc): drop the refusal cases main's relaxed geometry check no longer produces --- .../tests/execution/DepthPlaneGuestLayers.cpp | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp index e411ce0f3fd..cdb94c5363b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp @@ -28,7 +28,6 @@ #include <optional> #include <stdexcept> #include <string> -#include <string_view> #include <vector> namespace { @@ -346,19 +345,6 @@ void RunCubeGather(const Context& base, const WatchedBlock& block) { throw std::runtime_error("sampling a cube over six depth surfaces recorded " + std::to_string(imageToBufferCopies.load()) + " depth surface copies and " + std::to_string(bufferToImageCopies.load()) + " plane copy fills, expected 6 and 1"); } ClearDepthSurfaces(context.device); - const auto refused = [&](const GuestTextureResource& cube) { - try { - DepthSurfaceTexture(context, {}, cube, components); - } catch (const std::runtime_error& error) { - return std::string_view(error.what()).find("depth plane copy geometry does not match its layers") != std::string_view::npos; - } - return false; - }; - for (std::uint32_t layer = 0; layer < 5; ++layer) { - DepthSurfaceView(context, DepthTarget{address + geometry.GuestLayerOffset(layer), 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}); - } - Require(refused(resource), "a cube whose descriptor stride does not match one host depth slice and that is missing a face must be refused"); - ClearDepthSurfaces(context.device); } void RunCubeTiledSlice(const Context& base, const WatchedBlock& block) { From 93c85830907510660f79cb67216224b8a4e56a74 Mon Sep 17 00:00:00 2001 From: KernelPanicNT <76663133+KernelPanicNT@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:54:09 +0000 Subject: [PATCH 81/81] feat(libSceRudp): accept sceRudpEnableInternalIOThread2 like the first IO thread call The signature (uint32_t stackSize, uint32_t priority, SceKernelCpumask affinityMask) is the PS4 one from Orbital's IDA type database. It answers as sceRudpEnableInternalIOThread: RUDP_ERROR_NOT_INITIALIZED before sceRudpInit and success after it. Neither call starts a thread, since no context ever holds pending data to process, so the affinity is not applied, as the stack size and priority already are not. A second enable within one sceRudpInit/sceRudpEnd cycle is not modelled for either call: the Vita3K fork returns SCE_RUDP_ERROR_THREAD_IN_USE there and the console result is unknown. The test makes each enable call once per cycle and the debt entry records the gap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceRudp/Export.cpp | 6 +++--- core/libs/tests/GuestRudpPump.cpp | 11 +++++++++++ docs/dev/debt/libSceRudp.md | 3 ++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index 2bf31d06e5e..f62edcdc2ac 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -102,9 +102,9 @@ int APS5_VABI sceRudpEnableInternalIOThread(uint32_t stack_size, uint32_t priori return g_inited ? 0 : RUDP_ERROR_NOT_INITIALIZED; } -int APS5_VABI sceRudpEnableInternalIOThread2() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceRudpEnableInternalIOThread2(uint32_t stackSize, uint32_t priority, KernelCpumask affinityMask) { + (void)affinityMask; + return sceRudpEnableInternalIOThread(stackSize, priority); } int APS5_VABI sceRudpSetEventHandler(GuestRudpEventHandler handler, void* arg) { diff --git a/core/libs/tests/GuestRudpPump.cpp b/core/libs/tests/GuestRudpPump.cpp index 1e8ec7ab294..b941e47bfa2 100644 --- a/core/libs/tests/GuestRudpPump.cpp +++ b/core/libs/tests/GuestRudpPump.cpp @@ -13,6 +13,8 @@ int APS5_VABI sceRudpProcessEvents(); int APS5_VABI sceRudpNetFlush(); int APS5_VABI sceRudpGetMaxSegmentSize(std::uint16_t*); int APS5_VABI sceRudpNetReceived(); +int APS5_VABI sceRudpEnableInternalIOThread(std::uint32_t, std::uint32_t); +int APS5_VABI sceRudpEnableInternalIOThread2(std::uint32_t, std::uint32_t, std::uint64_t); } static void Require(bool value, int line) { @@ -32,6 +34,8 @@ int main() { CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == notInitialized); CHECK(segmentSize == 0x5a5a); CHECK(sceRudpGetMaxSegmentSize(nullptr) == notInitialized); + CHECK(sceRudpEnableInternalIOThread(0x4000, 700) == notInitialized); + CHECK(sceRudpEnableInternalIOThread2(0x4000, 700, 0x1) == notInitialized); alignas(8) static unsigned char pool[0xC58]; CHECK(sceRudpInit_nid_postfix(pool, sizeof(pool)) == 0); @@ -43,7 +47,14 @@ int main() { std::uint16_t segment[3] = {0, 0, 0x5a5a}; CHECK(sceRudpGetMaxSegmentSize(&segment[1]) == 0); CHECK(segment[0] == 0 && segment[1] == 1410 && segment[2] == 0x5a5a); + CHECK(sceRudpEnableInternalIOThread(0x4000, 700) == 0); CHECK(sceRudpEnd() == 0); + CHECK(sceRudpEnableInternalIOThread(0x4000, 700) == notInitialized); + + CHECK(sceRudpInit_nid_postfix(pool, sizeof(pool)) == 0); + CHECK(sceRudpEnableInternalIOThread2(0x4000, 700, 0x1) == 0); + CHECK(sceRudpEnd() == 0); + CHECK(sceRudpEnableInternalIOThread2(0x4000, 700, 0x1) == notInitialized); segmentSize = 0x5a5a; CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == notInitialized); diff --git a/docs/dev/debt/libSceRudp.md b/docs/dev/debt/libSceRudp.md index f2e4d7383b8..ec0e81990a0 100644 --- a/docs/dev/debt/libSceRudp.md +++ b/docs/dev/debt/libSceRudp.md @@ -4,9 +4,10 @@ - [sceRudpProcessEvents and sceRudpNetFlush](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host, and every `sceRudpWrite`, `sceRudpRead` and `sceRudpGetStatus` call here reports a reset or idle context without queueing data, so no context ever holds pending data to process or send; both calls return success after `sceRudpInit` and `RUDP_ERROR_NOT_INITIALIZED` before it, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) returns OK from its stubs. PPSA03490 calls `sceRudpProcessEvents(0)` followed by `sceRudpNetFlush()` from its P2P service thread and ignores the first result; whether the real calls take parameters is unknown - [sceRudpActivate](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host to activate, so it returns success without doing anything, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) does for its own `sceRudpActivate`. PPSA01325 calls it with no arguments during start-up; what the console activates, and whether it reports an error before `sceRudpInit`, is unverified +- [sceRudpEnableInternalIOThread and sceRudpEnableInternalIOThread2](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - return `RUDP_ERROR_NOT_INITIALIZED` before `sceRudpInit` and success after it without starting a thread: no context ever holds pending data to process (see `sceRudpProcessEvents` above), so the stack size, priority and affinity are not applied and no argument is validated. A second enable within one `sceRudpInit`/`sceRudpEnd` cycle, by either call, is not modelled and also returns success: the [Vita3K fork](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/d8a78af6ff734f01becb0876083a1c5d7d2630f8/vita3k/modules/SceRudp/SceLibRudp.cpp#L1013-L1014) cited below returns `SCE_RUDP_ERROR_THREAD_IN_USE` ([`0x80770010`](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/d8a78af6ff734f01becb0876083a1c5d7d2630f8/vita3k/modules/SceRudp/SceLibRudp.cpp#L49)) from its `sceRudpEnableInternalIOThread` once its thread has started (its `sceRudpEnableInternalIOThread2` is unimplemented), and the console result is unknown. The second call's `(uint32_t stackSize, uint32_t priority, SceKernelCpumask affinityMask)` signature is the PS4 one from the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L1970) IDA type database; the PS5 signature is unverified ### Unknown function info - [sceRudpGetStatus](../../../core/libs/prx/libSceRudp/Export.cpp) - PPSA01521 passes an output pointer and byte count `0xF8`. Measured on a PS5 (firmware 13.60): the status is 0xF8 bytes; before `sceRudpInit` every call returns `RUDP_ERROR_NOT_INITIALIZED`, and after it a null pointer or a byte count of 0 or above 0xF8 returns `RUDP_ERROR_INVALID_ARGUMENT`. The layout matches `SceRudpStatus` in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp) with `currentContexts` at 0x64 and `allocs`, `frees`, `memCurrent` and `memPeak` at 0xC8 to 0xD4. Only `currentContexts` is reported: the traffic counters stay 0 because nothing is sent, and the allocator counters stay 0 because the library does not allocate from the title's pool (the console reports 6 allocations and 0x1260 bytes right after `sceRudpInit`, and 4 more allocations and 0x3F0 bytes per context). The console also writes `currentContexts` and the allocator counters when the byte count is smaller than their offset; only the requested bytes are written here. - [sceRudpGetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - measured on a PS5 (firmware 13.60) in #3073: before `sceRudpInit` it returns `RUDP_ERROR_NOT_INITIALIZED` and leaves the value untouched, also for a null pointer; after it a null pointer returns `RUDP_ERROR_INVALID_ARGUMENT` and a valid one gets 1410 with result 0. The library keeps no configurable segment size (there is no `sceRudpSetMaxSegmentSize`), so 1410 is always reported. The `uint16_t*` parameter is inferred: the measurement does not show how many bytes the console writes, PPSA03490 passes a pointer to a 16-bit value and [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.cpp) declares the PS3 `cellRudpSetMaxSegmentSize(u16 mss)`, so only 16 bits are written -- PPSA06888 imports declared without parameters, signatures unknown: [sceRudpEnableInternalIOThread2, sceRudpGetOption, sceRudpGetRemoteInfo](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp), imported by its bundled `Media/Plugins/SceRudp.prx` and not called during start-up. Names from the NID table of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp), which declares them without parameters too +- PPSA06888 imports declared without parameters, signatures unknown: [sceRudpGetOption, sceRudpGetRemoteInfo](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp), imported by its bundled `Media/Plugins/SceRudp.prx` and not called during start-up. Names from the NID table of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp), which declares them without parameters too