From 82ac3bd93ea64ecdc3f8a7a050639f35c683c01f Mon Sep 17 00:00:00 2001 From: Mouhand Kaddo <73709175+Mouhand-Kaddo@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:00:36 +0400 Subject: [PATCH] fix(agc): skip read-only depth resummarize draws --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 +++++ .../src/Driver/Draw/DrawPrecheck.cpp | 1 + .../Graphics/include/State.hpp | 1 + .../libSceAgcDriver/Graphics/src/State.cpp | 15 +++++++++++ .../prx/libSceAgcDriver/tests/Graphics.cpp | 26 +++++++++++++++++++ .../libs/prx/libSceAgcDriver/tests/Submit.cpp | 21 ++++++++++++++- docs/dev/TechnicalDebt.md | 2 +- 7 files changed, 70 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 6d3ef615a..b2b5719b1 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1005,6 +1005,12 @@ target_include_directories(agc_driver_prepared_shaders_tests PRIVATE if(BUILD_TESTING) set_target_properties(agc_shader_mode_register_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_driver_readonly_resummarize COMMAND agc_driver_tests --readonly-resummarize) + set_tests_properties(agc_driver_readonly_resummarize PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(agc_driver_readonly_resummarize PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_shader_mode_register COMMAND agc_shader_mode_register_tests) set_tests_properties(agc_shader_mode_register PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 20) if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp index 03a3f4533..267b6cf70 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp @@ -23,6 +23,7 @@ std::optional Driver::precheckDraw(const QueueState& queue, const S localDevice->ColorMetadataPass(*pass); return DrawVerdict::Drawn; } + if (Graphics::DepthResummarizeDrawsNothing(queue)) return DrawVerdict::Nothing; rejected = Graphics::DepthMaintenanceRejection(queue); if (!rejected.empty()) return DrawVerdict::Rejected; static const bool traceIndirectEnabled = std::getenv("APS5_TRACE_INDIRECT_DRAWS") != nullptr; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 0012dbfa5..14749855f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -129,6 +129,7 @@ struct ColorMetadataPass { }; std::optional DecodeColorMetadataPass(const QueueState& queue); std::string DepthMaintenanceRejection(const QueueState& queue); +bool DepthResummarizeDrawsNothing(const QueueState& queue); // The message DecodeState (or the pixel stage decode after it) would throw for the register rules // this precheck covers, evaluated without exceptions before the draw is decoded; empty when they // pass (DecodeState still checks everything). A register a rule needs that is absent is no verdict. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index ae2dcb330..65028c511 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -545,7 +545,22 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { return result; } +bool DepthResummarizeDrawsNothing(const QueueState& queue) { + const auto& cx = queue.context; + const auto control = find(cx, 0x000); + const auto depthControl = find(cx, 0x200); + const auto zInfo = find(cx, 0x010); + const auto stencilInfo = find(cx, 0x011); + const auto targetMask = find(cx, 0x8e); + const auto shaderMask = find(cx, 0x8f); + if (control == cx.end() || control->second != 0x10u || depthControl == cx.end() || zInfo == cx.end() || stencilInfo == cx.end() || targetMask == cx.end() || shaderMask == cx.end()) return false; + const auto depth = depthControl->second; + const auto format = zInfo->second & 3u; + return (depth & 2u) != 0 && (depth & 0xc000000cu) == 0 && depthPassThrough(depth) && (format == 1u || format == 3u) && (stencilInfo->second & 1u) == 0 && ColorWriteMask(cx) == 0 && PixelProgramSkipped(queue); +} + std::string DepthMaintenanceRejection(const QueueState& queue) { + if (DepthResummarizeDrawsNothing(queue)) return {}; const auto control = find(queue.context, 0x000); if (control == queue.context.end() || (control->second & ~0x2063u) == 0) return {}; const auto depthControl = find(queue.context, 0x200); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 4e26034dc..ccce1307e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1297,6 +1297,32 @@ void depthMaintenanceTests() { auto absent = makeState(); absent.context.erase(0x000); Require(AgcDriver::Graphics::DepthMaintenanceRejection(absent).empty(), "an absent depth control produced a maintenance verdict"); + auto readOnly = makeState(); + readOnly.context[0x000] = 0x10; + readOnly.context[0x200] = 0x007007f3; + readOnly.context[0x010] = 0xa0000183; + readOnly.context[0x011] = 0x20000180; + readOnly.context[0x8e] = 0xffffffff; + readOnly.context[0x8f] = 0; + readOnly.shader.erase(0x8); + readOnly.shader.erase(0x9); + Require(AgcDriver::Graphics::DepthMaintenanceRejection(readOnly).empty(), "a resummarize draw with pass-through read-only depth and no stencil plane was rejected"); + for (const auto [offset, value] : {std::pair{0x200u, 0x007007f7u}, {0x200u, 0x007007fbu}, {0x200u, 0x007007e3u}, {0x011u, 0x20000181u}, {0x8fu, 0xfu}, {0x000u, 0x11u}, {0x010u, 0xa0000182u}}) { + auto changed = readOnly; + changed.context[offset] = value; + Require(!AgcDriver::Graphics::DepthMaintenanceRejection(changed).empty(), "a resummarize draw with surface work or unsupported depth passed"); + } + auto pixel = readOnly; + pixel.shader[0x8] = 1; + pixel.shader[0x9] = 0; + pixel.context[0x1c4] = 0; + pixel.context[0x203] = 0x40; + Require(!AgcDriver::Graphics::DepthMaintenanceRejection(pixel).empty(), "a resummarize draw with a running pixel stage passed"); + for (const auto offset : {0x200u, 0x010u, 0x011u, 0x8eu, 0x8fu}) { + auto missing = readOnly; + missing.context.erase(offset); + Require(!AgcDriver::Graphics::DepthMaintenanceRejection(missing).empty(), "a resummarize draw without a required register passed"); + } } // SPI_SHADER_Z_FORMAT (0x1c4) and the export enables of DB_SHADER_CONTROL (0x203): Z export needs a diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index da1c9ba0b..ec50e84c9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #ifdef _WIN32 @@ -111,6 +112,17 @@ void testValidation() { AgcDriverWaitIdle_nid_postfix(); } +void testReadOnlyResummarize() { + std::vector commands; + for (const auto [offset, value] : {std::pair{0x000u, 0x10u}, {0x200u, 0x007007f3u}, {0x010u, 0xa0000183u}, {0x011u, 0x20000180u}, {0x8eu, 0xffffffffu}, {0x8fu, 0u}, {0x1c4u, 0u}, {0x203u, 0u}}) { + commands.insert(commands.end(), {0xc0016900u, offset, value}); + } + commands.insert(commands.end(), {0xc0012d00u, 3u, 2u}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + check(sceAgcDriverSubmitDcb(&packet) == 0, "read-only resummarize submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + void testClearState() { AgcDriver::QueueState graphics{{{0x20c, 1}}, {{0x10, 17}, {0x11, 23}}, {{0x242, 5}}}; const auto shader = graphics.shader; @@ -671,8 +683,14 @@ void testWorkerFailure() { } -int main() { +int main(int argc, char** argv) { try { + if (argc == 2 && std::string_view(argv[1]) == "--readonly-resummarize") { + testReadOnlyResummarize(); + LibcRunShutdown_nid_postfix(); + std::puts("Read-only resummarize submission passed"); + return 0; + } const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; alignas(256) std::array rawCode{}; @@ -766,6 +784,7 @@ int main() { testHeaderWithoutProgramAddress(); testCopyIntoHostMemory(); testRegisteredFloatMode(); + testReadOnlyResummarize(); testWorkerFailure(); check(expectFailure([] { LibcRunShutdown_nid_postfix(); }).find("required shader register") != std::string::npos, "shutdown lost worker failure"); std::puts("AGC driver submit tests passed"); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 43a5c5397..29e1b4a74 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -608,7 +608,7 @@ Throughout the project, every function at every stage either **does exactly what - [Draw snapshot buffer descriptors](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) rewrite each affected array from element zero, preserving unchanged descriptors. MoltenVK 1.4.2 writes auxiliary buffer lengths at index `i` instead of `start + i` for partial array updates ([MVKDescriptorSet.mm, BufferAuxSize](https://github.com/KhronosGroup/MoltenVK/blob/v1.4.2/MoltenVK/MoltenVK/GPUObjects/MVKDescriptorSet.mm#L1051)). Per-element writes therefore let one buffer length replace another even when Vulkan buffer handles and ranges are correct. - [Static scalar calls](../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require an ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and a later `s_setpc_b64` return reading that exact pair. A pair serves one call, or several calls whose regions are disjoint, each closed by the first return reading the pair after its target. Outside every region of its calls the pair may only be overwritten whole, by `s_mov_b32`, `s_mov_b64`, an SOP2 result or a scalar load, as the caller of a user-data callee does before each call. Regions must nest or be disjoint; recursion, other shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. Arbitrary indirect calls and general call frames are not implemented. - [User-data callees](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is a user-data pair, or a copy of one made with `s_mov_b32` or `s_mov_b64` in the same block, calls the code at the address that user data holds. At dispatch the callee is read from guest memory up to its `s_setpc_b64` on the link pair, one copy of it is appended per call after the caller's last instruction, and the call is rewritten to `s_call_b64` to that copy. The words after the caller's last instruction are dropped; in the shaders seen they are metadata, and a caller that reads its program counter, which could address them, throws. A copy runs at another program counter than the callee, so a callee that reads its program counter, makes a call, ends the program, jumps through another pair, returns before code it branches to, spans more than 64 KiB or is not readable throws. Callees are read again whenever a dispatch misses the dispatch cache; a dispatch that hits it reuses the linked program, so a title that rewrote a callee in place at the same address would keep running the old code. -- [DB_RENDER_CONTROL resummarize draws](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - HTILE is not modeled, so a RESUMMARIZE_ENABLE draw without depth or stencil tests and without color writes changes no modeled state and draws nothing; any other resummarize, copy or decompress draw throws +- [DB_RENDER_CONTROL resummarize draws](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - HTILE is not modeled, so a RESUMMARIZE_ENABLE draw without depth or stencil tests and without color writes changes no modeled state and draws nothing. A pure resummarize with pass-through D16/D32 depth, no depth writes or bounds test, no stencil plane, no color writes and no running pixel stage also draws nothing; other resummarize, copy or decompress draws throw. - [s_call_b64](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) saves `PC + 4` over `GetShaderBase` and branches to `PC + 4 + signext(SIMM16) * 4`, following [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) sections 3.6.3, 12.2 and 12.3. Forward callees after `s_endpgm` are decoded. Measured on RDNA2 (gfx1036) in wave32 and wave64 with SIMM16 = 3: the link pair receives `PC + 4`, the branch goes to `PC + 4 + signext(SIMM16) * 4`, and `s_setpc_b64` of the link returns to the instruction after the call. Execution on the PS5 (gfx1013) and title compatibility for these bounded scalar calls remain unverified; `s_rfe_b64` still throws in translation. - [sceVideodec2CreateDecoder](../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) decodes HEVC (codec type 974921) and VP9 (2382845) through FFmpeg, besides H.264. The tags come from SharpProspero's `Videodec2CodecType` and EVO-PLAYER-PS5's `sce_videodec2.h`, which records them from hardware runs. HEVC input is converted from length-prefixed NAL units to Annex B as for H.264, and VP9 frames are passed through. Output is the same 8-bit NV12 for every codec; a decoder for HEVC Main10 or VP9 Profile 2 is created, but a frame deeper than 8 bits throws, since the console's 10-bit output layout is unknown, and `QueryDecoderMemoryInfo` reports the 8-bit frame size for it. `sceVideodec2GetPictureInfo` and `sceVideodec2GetAvcPictureInfo` throw for HEVC and VP9 pictures, which use other layouts (`SceVideodec2HevcPictureInfo`, `SceVideodec2Vp9PictureInfo`), and `sceVideodec2GetHevcPictureInfo` and `sceVideodec2GetVp9PictureInfo` are not exported - [syslog](../../core/libs/prx/libc/src/Syslog.cpp) (libc) writes each message to `stderr` as `[syslog:] `, with a newline added when missing, instead of sending it to the console's system log. Priority and facility handling follow FreeBSD's `vsyslog`: `%m` expands to the guest errno's message, unknown bits are reported (as priority 0x23, since FreeBSD passes `LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID` as the priority) and masked, a missing facility becomes `LOG_USER`, and errno is preserved. `setlogmask` follows [FreeBSD 11](https://github.com/freebsd/freebsd-src/blob/8d2708de5d6112a492744d367cfdfb8fd219227a/lib/libc/gen/syslog.c): the process-wide mask defaults to 0xff, zero queries it, and nonzero values replace it; masked messages are skipped before formatting, including the unknown-priority diagnostic. The console's mask behaviour is unverified. `openlog` and `closelog` are not implemented, so their identity and options are not applied